Repository: mscroggs/symfem

Issue:
Add as_tikz function to plots

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [155 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=mscroggs_symfem&pullRequest=181)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mscroggs_symfem&pullRequest=181&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=mscroggs_symfem&pullRequest=181&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=mscroggs_symfem&pullRequest=181&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.gitignore b/.gitignore
index 312cbe4..fc6fbcd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,4 @@ docs/_build
 joss/build.sh
 joss/paper.pdf
 
-test/test-output-*
-
 _temp/
diff --git a/setup.py b/setup.py
index b13d703..ad8a0ef 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@ if __name__ == "__main__":
         install_requires=["sympy>=1.10"],
         extras_require={
             "style": ["flake8", "pydocstyle", "mypy"],
-            "docs": ["sphinx", "sphinx-autoapi"],
+            "docs": ["sphinx==5.0.2", "sphinx-autoapi"],
             "optional": ["svgwrite", "CairoSVG"],
             "test": ["pytest", "symfem[optional]", "numpy"],
         }
diff --git a/symfem/__init__.py b/symfem/__init__.py
index 06f65e3..18716d4 100644
--- a/symfem/__init__.py
+++ b/symfem/__init__.py
@@ -4,3 +4,5 @@ from .create import add_element, create_reference, create_element
 from .version import version as __version__
 
 __citation__ = "https://doi.org/10.21105/joss.03556 (Scroggs, 2021)"
+__github__ = "https://github.com/mscroggs/symfem"
+__git__ = "https://github.com/mscroggs/symfem.git"
diff --git a/symfem/basis_functions.py b/symfem/basis_functions.py
index 2233f4e..37182d0 100644
--- a/symfem/basis_functions.py
+++ b/symfem/basis_functions.py
@@ -190,7 +190,7 @@ class SubbedBasisFunction(BasisFunction):
 
     def plot_values(
         self, reference: Reference, img: typing.Any,
-        scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
+        value_scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
     ):
         """Plot the function's values."""
-        self.f.plot_values(reference, img, scale, n)
+        self.f.plot_values(reference, img, value_scale, n)
diff --git a/symfem/finite_element.py b/symfem/finite_element.py
index afef751..f20fb9b 100644
--- a/symfem/finite_element.py
+++ b/symfem/finite_element.py
@@ -89,7 +89,9 @@ class FiniteElement(ABC):
         else:
             raise ValueError(f"Unknown order: {order}")
 
-    def plot_basis_function(self, n: int, filename: str, width: int = None, height: int = None):
+    def plot_basis_function(
+        self, n: int, filename: str, **kwargs: typing.Any
+    ):
         """Plot a diagram showing a basis function."""
         f = self.get_basis_functions()[n]
         values = self.tabulate_basis(self.reference.make_lattice(6), "xyz,xyz")
@@ -98,8 +100,8 @@ class FiniteElement(ABC):
             assert isinstance(row, tuple)
             for i in row:
                 max_v = max(max_v, parse_function_input(i).norm())
-        scale = 1 / max_v
-        f.plot(self.reference, filename, None, None, None, n, scale, width=width, height=height)
+        value_scale = 1 / max_v
+        f.plot(self.reference, filename, None, None, None, n, value_scale, **kwargs)
 
     @abstractmethod
     def map_to_cell(
@@ -347,7 +349,9 @@ class CiarletElement(FiniteElement):
 
         return self._basis_functions
 
-    def plot_basis_function(self, n: int, filename: str, width: int = None, height: int = None):
+    def plot_basis_function(
+        self, n: int, filename: str, **kwargs: typing.Any
+    ):
         """Plot a diagram showing a basis function."""
         f = self.get_basis_functions()[n]
         d = self.dofs[n]
@@ -357,15 +361,15 @@ class CiarletElement(FiniteElement):
             assert isinstance(row, tuple)
             for i in row:
                 max_v = max(max_v, parse_function_input(i).norm())
-        scale = 1 / max_v
-        f.plot(self.reference, filename, d.dof_point(), d.dof_direction(), d.entity, n, scale,
-               width=width, height=height)
+        value_scale = 1 / max_v
+        f.plot(self.reference, filename, d.dof_point(), d.dof_direction(), d.entity, n,
+               value_scale, **kwargs)
 
-    def plot_dof_diagram(self, filename: str, width: int = None, height: int = None):
+    def plot_dof_diagram(
+        self, filename: str, plot_options: typing.Dict[str, typing.Any] = {}, **kwargs: typing.Any
+    ):
         """Plot a diagram showing the DOFs of the element."""
-        assert filename.endswith(".svg") or filename.endswith(".png")
-
-        img = Picture(width=width, height=height)
+        img = Picture(**kwargs)
 
         dofs_by_subentity: typing.Dict[int, typing.Dict[int, ListOfFunctionals]] = {
             i: {j: [] for j in range(self.reference.sub_entity_count(i))}
@@ -402,7 +406,7 @@ class CiarletElement(FiniteElement):
                         img.add_dof_marker(
                             d.dof_point(), self.dofs.index(d), colors.entity(d.entity[0]))
 
-        img.save(filename)
+        img.save(filename, plot_options=plot_options)
 
     def map_to_cell(
         self, vertices_in: SetOfPointsInput, basis: typing.List[AnyFunction] = None,
diff --git a/symfem/functions.py b/symfem/functions.py
index c07a708..51678fc 100644
--- a/symfem/functions.py
+++ b/symfem/functions.py
@@ -250,8 +250,8 @@ class AnyFunction(ABC):
     def plot(
         self, reference: Reference, filename: str, dof_point: PointType = None,
         dof_direction: PointType = None, dof_entity: typing.Tuple[int, int] = None,
-        dof_n: int = None, scale: sympy.core.expr.Expr = sympy.Integer(1),
-        width: int = None, height: int = None
+        dof_n: int = None, value_scale: sympy.core.expr.Expr = sympy.Integer(1),
+        plot_options: typing.Dict[str, typing.Any] = {}, **kwargs: typing.Any
     ):
         """Plot the function."""
         from .plotting import Picture, colors
@@ -260,7 +260,7 @@ class AnyFunction(ABC):
         if self.is_scalar:
             extra = (0, )
 
-        img = Picture(width=width, height=height)
+        img = Picture(**kwargs)
 
         if dof_entity is not None and dof_entity[0] > 1:
             sub_e = reference.sub_entity(*dof_entity)
@@ -277,22 +277,22 @@ class AnyFunction(ABC):
                         reference.vertices[reference.edges[entity][1]] + extra, c)
 
                 if dim == reference.tdim:
-                    self.plot_values(reference, img, scale)
+                    self.plot_values(reference, img, value_scale)
 
-                if (dim, entity) == dof_entity:
-                    if dof_direction is not None:
-                        assert dof_point is not None and dof_n is not None
-                        img.add_dof_arrow(dof_point + extra, dof_direction + extra, dof_n,
-                                          colors.PURPLE, bold=False)
-                    elif dof_point is not None:
-                        assert dof_n is not None
-                        img.add_dof_marker(dof_point + extra, dof_n, colors.PURPLE, bold=False)
+            if (dim, entity) in ze:
+                if dof_direction is not None:
+                    assert dof_point is not None and dof_n is not None
+                    img.add_dof_arrow(dof_point + extra, dof_direction + extra, dof_n,
+                                      colors.PURPLE, bold=False)
+                elif dof_point is not None:
+                    assert dof_n is not None
+                    img.add_dof_marker(dof_point + extra, dof_n, colors.PURPLE, bold=False)
 
-        img.save(filename)
+        img.save(filename, plot_options=plot_options)
 
     def plot_values(
         self, reference: Reference, img: typing.Any,
-        scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
+        value_scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
     ):
         """Plot the function's values."""
         raise ValueError(f"Cannot plot function of type '{self.__class__.__name__}'")
@@ -554,7 +554,7 @@ class ScalarFunction(AnyFunction):
 
     def plot_values(
         self, reference: Reference, img: typing.Any,
-        scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
+        value_scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
     ):
         """Plot the function's values."""
         from .plotting import Picture, colors
@@ -562,12 +562,12 @@ class ScalarFunction(AnyFunction):
 
         pts, pairs = reference.make_lattice_with_lines(n)
 
-        scale *= sympy.Rational(5, 8)
+        value_scale *= sympy.Rational(5, 8)
 
         deriv = self.grad(reference.tdim)
         evals = []
         for p in pts:
-            value = self.subs(x, p).as_sympy() * scale
+            value = self.subs(x, p).as_sympy() * value_scale
             assert isinstance(value, sympy.core.expr.Expr)
             evals.append(value)
 
@@ -581,8 +581,8 @@ class ScalarFunction(AnyFunction):
             assert isinstance(di, sympy.core.expr.Expr)
             assert isinstance(dj, sympy.core.expr.Expr)
             img.add_bezier(
-                tuple(pi) + (evals[i], ), tuple(d_pi) + (evals[i] + di * scale, ),
-                tuple(d_pj) + (evals[j] + dj * scale, ), tuple(pj) + (evals[j], ),
+                tuple(pi) + (evals[i], ), tuple(d_pi) + (evals[i] + di * value_scale, ),
+                tuple(d_pj) + (evals[j] + dj * value_scale, ), tuple(pj) + (evals[j], ),
                 colors.ORANGE)
 
 
@@ -825,7 +825,7 @@ class VectorFunction(AnyFunction):
 
     def plot_values(
         self, reference: Reference, img: typing.Any,
-        scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
+        value_scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
     ):
         """Plot the function's values."""
         from .plotting import Picture, colors
@@ -834,7 +834,7 @@ class VectorFunction(AnyFunction):
         pts = reference.make_lattice(n)
 
         for p in pts:
-            value = self.subs(x, p) * scale / 4
+            value = self.subs(x, p) * value_scale / 4
             size = float(value.norm() * 40)
             img.add_arrow(p, VectorFunction(p) + value, colors.ORANGE, size)
 
diff --git a/symfem/piecewise_functions.py b/symfem/piecewise_functions.py
index cfd1b91..03ded68 100644
--- a/symfem/piecewise_functions.py
+++ b/symfem/piecewise_functions.py
@@ -375,7 +375,7 @@ class PiecewiseFunction(AnyFunction):
 
     def plot_values(
         self, reference: Reference, img: typing.Any,
-        scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
+        value_scale: sympy.core.expr.Expr = sympy.Integer(1), n: int = 6
     ):
         """Plot the function's values."""
         from .plotting import Picture
@@ -397,4 +397,4 @@ class PiecewiseFunction(AnyFunction):
                     raise ValueError("Unsupported cell type")
             else:
                 raise ValueError("Unsupported tdim")
-            f.plot_values(ref, img, scale, n // 2)
+            f.plot_values(ref, img, value_scale, n // 2)
diff --git a/symfem/plotting.py b/symfem/plotting.py
index 657d723..57d8cf6 100644
--- a/symfem/plotting.py
+++ b/symfem/plotting.py
@@ -14,6 +14,29 @@ SVGFormat = typing.List[typing.Tuple[
     str, typing.Tuple[typing.Any, ...], typing.Dict[str, typing.Any]]]
 
 
+def tex_font_size(n: int):
+    """Convert a font size to a tex size command."""
+    if n < 21:
+        return "\\tiny"
+    if n < 24:
+        return "\\scriptsize"
+    if n < 27:
+        return "\\footnotesize"
+    if n < 30:
+        return "\\small"
+    if n < 33:
+        return "\\normalsize"
+    if n < 36:
+        return "\\large"
+    if n < 39:
+        return "\\Large"
+    if n < 42:
+        return "\\LARGE"
+    if n < 45:
+        return "\\huge"
+    return "\\Huge"
+
+
 class Colors:
     """Class storing colours used in diagrams."""
 
@@ -25,6 +48,10 @@ class Colors:
     PURPLE = "#DD2299"
     GRAY = "#AAAAAA"
 
+    def __init__(self):
+        """Initialise."""
+        self._tikz = {}
+
     def entity(self, n: int) -> str:
         """Get the color used for an entity of a given dimension.
 
@@ -44,6 +71,22 @@ class Colors:
             return self.PURPLE
         raise ValueError(f"Unsupported dimension: {n}")
 
+    def get_tikz_name(self, name: str) -> str:
+        """Get the name of the colour to be used in Tikz."""
+        if name.startswith("#"):
+            if name not in self._tikz:
+                self._tikz[name] = f"customcolor{len(self._tikz)}"
+            return self._tikz[name]
+        return name
+
+    def get_tikz_definitions(self) -> str:
+        """Get the definitions of colours used in Tikz."""
+        out = ""
+        for a, b in self._tikz.items():
+            assert a.startswith("#")
+            out += f"\\definecolor{{{b}}}{{HTML}}{{{a[1:]}}}\n"
+        return out
+
 
 colors = Colors()
 
@@ -69,6 +112,20 @@ class PictureElement(ABC):
         """
         pass
 
+    @abstractmethod
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        pass
+
     @property
     @abstractmethod
     def points(self) -> SetOfPoints:
@@ -148,6 +205,22 @@ class Line(PictureElement):
             "line", (map_pt(self.start), map_pt(self.end)),
             {"stroke": self.color, "stroke_width": self.width, "stroke_linecap": "round"})]
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        s = map_pt(self.start)
+        e = map_pt(self.end)
+        return (f"\\draw[{colors.get_tikz_name(self.color)},line width={self.width * 0.2}pt,"
+                f"line cap=round] ({s[0]},{s[1]}) -- ({e[0]},{e[1]});\n")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -194,16 +267,35 @@ class Bezier(PictureElement):
         Returns:
             A list of svgwrite functions to call, with args tuples and kwargs dictionaries
         """
-        a = map_pt(self.start)
-        b = map_pt(self.mid1)
-        c = map_pt(self.mid2)
-        d = map_pt(self.end)
+        s = map_pt(self.start)
+        m1 = map_pt(self.mid1)
+        m2 = map_pt(self.mid2)
+        e = map_pt(self.end)
         return [(
             "path", tuple(),
-            {"d": (f"M{a[0]},{a[1]} C{b[0]},{b[1]}, {c[0]},{c[1]}, {d[0]},{d[1]}"),
+            {"d": (f"M{s[0]},{s[1]} C{m1[0]},{m1[1]}, {m2[0]},{m2[1]}, {e[0]},{e[1]}"),
              "stroke": self.color, "stroke_width": self.width, "stroke_linecap": "round",
              "fill": "none"})]
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        s = map_pt(self.start)
+        m1 = map_pt(self.mid1)
+        m2 = map_pt(self.mid2)
+        e = map_pt(self.end)
+        return (f"\\draw[{colors.get_tikz_name(self.color)},line width={self.width * 0.2}pt,"
+                f"line cap=round] ({s[0]},{s[1]}) .. controls ({m1[0]},{m1[1]}) "
+                f"and ({m2[0]},{m2[1]}) .. ({e[0]},{e[1]});\n")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -267,6 +359,23 @@ class Arrow(PictureElement):
                 {"stroke": self.color, "stroke_width": self.width, "stroke_linecap": "round"}))
         return out
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        s = map_pt(self.start)
+        e = map_pt(self.end)
+        return (f"\\draw[-stealth,{colors.get_tikz_name(self.color)},"
+                f"line width={self.width * 0.2}pt,line cap=round] "
+                f"({s[0]},{s[1]}) -- ({e[0]},{e[1]});\n")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -339,6 +448,24 @@ class NCircle(PictureElement):
 
         return out
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        c = map_pt(self.centre)
+        return (f"\\draw[{colors.get_tikz_name(self.color)},line width={self.width * 0.2}pt,"
+                f"fill={colors.get_tikz_name(self.fill_color)}] "
+                f"({c[0]},{c[1]}) circle ({self.radius * 0.2}pt);\n"
+                f"\\node[{colors.get_tikz_name(self.text_color)},anchor=center] "
+                f"at ({c[0]},{c[1]}) {{{tex_font_size(self.font_size)} {self.number}}};")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -378,6 +505,21 @@ class Fill(PictureElement):
         return [("polygon", (tuple(map_pt(p) for p in self.vertices), ),
                 {"fill": self.color, "opacity": self.opacity})]
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        vs = [map_pt(v) for v in self.vertices]
+        return (f"\\fill[{colors.get_tikz_name(self.color)},opacity={self.opacity}]"
+                " " + " -- ".join([f"({v[0]},{v[1]})" for v in vs]) + " -- cycle;")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -422,6 +564,21 @@ class Math(PictureElement):
              "style": "text-anchor:middle;dominant-baseline:middle;"
                       "font-family:'CMU Serif',serif;font-style:italic"})]
 
+    def as_tikz(
+        self, map_pt: typing.Callable[[PointType], typing.Tuple[float, float]]
+    ) -> str:
+        """Return Tikz format.
+
+        Args:
+            map_pt: A function that adjust the origin and scales the picture
+
+        Returns:
+            A Tikz string
+        """
+        p = map_pt(self.point)
+        return (f"\\node[{colors.get_tikz_name(self.color)},anchor=center] "
+                f"at ({p[0]},{p[1]}) {{{tex_font_size(self.font_size)}${self.math}$}};")
+
     @property
     def points(self) -> SetOfPoints:
         """Get set of points used by this element.
@@ -438,16 +595,24 @@ class Picture:
     axes_3d: SetOfPoints
 
     def __init__(
-        self, padding: sympy.core.expr.Expr = sympy.Integer(25), width: int = None,
-        height: int = None, axes_3d: SetOfPointsInput = None
+        self, padding: sympy.core.expr.Expr = sympy.Integer(25), scale: int = None,
+        width: int = None, height: int = None, axes_3d: SetOfPointsInput = None,
+        dof_arrow_size: typing.Union[int, sympy.core.expr.Expr] = 1,
+        title: str = None, desc: str = None, svg_metadata: str = None, tex_comment: str = None
     ):
         """Create a picture.
 
         Args:
             padding: The padding between the objects and the edge of the picture
+            scale: The amount to scale the coordinates by
             width: The width of the picture
             height: The height of the picture
             axes_3d: The axes to use when drawing a 3D object
+            dof_arrow_size: The relative length of the DOF arrows
+            title: The title of the picture
+            desc: A description of the picture
+            svg_metadata: Metadata to put at the start of the SVG file
+            tex_comment: Comment to put at the start of the TeX file
         """
         self._default_axes: SetOfPoints = (
             (sympy.Integer(1), -sympy.Rational(2, 25)),
@@ -455,8 +620,36 @@ class Picture:
             (sympy.Integer(0), sympy.Integer(1)))
         self.elements: typing.List[PictureElement] = []
         self.padding = padding
+        self.scale = scale
         self.height = height
+        if isinstance(dof_arrow_size, int):
+            self.dof_arrow_size = sympy.Integer(dof_arrow_size)
+        else:
+            assert isinstance(dof_arrow_size, sympy.core.expr.Expr)
+            self.dof_arrow_size = dof_arrow_size
         self.width = width
+        self.title = title
+        self.desc = desc
+        if svg_metadata is None:
+            import symfem
+            self.svg_metadata = (
+                "<!--\n"
+                "This diagram was created using Symfem\n"
+                f"{symfem.__github__}\n"
+                f"{symfem.__citation__}\n"
+                "-->\n")
+        else:
+            assert isinstance(svg_metadata, str)
+            self.svg_metadata = svg_metadata
+        if tex_comment is None:
+            import symfem
+            self.tex_comment = (
+                "% This diagram was created using Symfem\n"
+                f"% {symfem.__github__}\n"
+                f"% {symfem.__citation__}\n\n")
+        else:
+            assert isinstance(tex_comment, str)
+            self.tex_comment = tex_comment
 
         if axes_3d is None:
             self.axes_3d = self._default_axes
@@ -555,6 +748,7 @@ class Picture:
         """Add a DOF arrow."""
         vdirection = VectorFunction(self.parse_point(direction))
         vdirection /= 8 * vdirection.norm()
+        vdirection *= self.dof_arrow_size
         start = VectorFunction(self.parse_point(point))
         if shifted:
             start += vdirection / 3
@@ -589,50 +783,88 @@ class Picture:
         """Add a filled polygon to the picture."""
         self.elements.append(Fill(tuple(self.parse_point(p) for p in vertices), color, opacity))
 
-    def as_svg(self, filename: str = None) -> str:
-        """Convert to an SVG."""
-        try:
-            import svgwrite
-        except ImportError:
-            raise ImportError("svgwrite is needed for plotting SVGs"
-                              " (pip install svgwrite)")
-
+    def compute_scale(self, unit: str = "px", reverse_y: bool = True) -> typing.Tuple[
+        sympy.core.expr.Expr, sympy.core.expr.Expr, sympy.core.expr.Expr,
+        typing.Callable[[PointType], typing.Tuple[float, float]]
+    ]:
+        """Compute the scale and size of the picture."""
         minx = min(i.minx() for i in self.elements)
         miny = min(i.miny() for i in self.elements)
         maxx = max(i.maxx() for i in self.elements)
         maxy = max(i.maxy() for i in self.elements)
 
-        if self.width is not None:
+        if self.scale is not None:
+            assert self.width is None
+            assert self.height is None
+            scale = sympy.Integer(self.scale)
+        elif self.width is not None:
             assert self.height is None
-            scale = self.width / (maxx - minx)
+            scale = sympy.Integer(self.width) / (maxx - minx)
         elif self.height is not None:
-            assert self.width is None
-            scale = self.height / (maxy - miny)
+            scale = sympy.Integer(self.height) / (maxy - miny)
+        else:
+            scale = sympy.Integer(450)
+
+        if unit == "mm":
+            scale /= 20
+        elif unit == "cm":
+            scale /= 200
+        elif unit == "px":
+            pass
         else:
-            scale = 450
+            raise ValueError(f"Unknown unit: {unit}")
 
         width = 2 * self.padding + (maxx - minx) * scale
         height = 2 * self.padding + (maxy - miny) * scale
 
+        if reverse_y:
+            def map_pt(pt: PointType) -> typing.Tuple[float, float]:
+                """Map a point."""
+                return (
+                    float(self.padding + (pt[0] - minx) * scale),
+                    float(height - self.padding - (pt[1] - miny) * scale))
+        else:
+            def map_pt(pt: PointType) -> typing.Tuple[float, float]:
+                """Map a point."""
+                return (
+                    float(self.padding + (pt[0] - minx) * scale),
+                    float(self.padding + (pt[1] - miny) * scale))
+
+        return scale, height, width, map_pt
+
+    def as_svg(self, filename: str = None) -> str:
+        """Convert to an SVG."""
+        try:
+            import svgwrite
+        except ImportError:
+            raise ImportError("svgwrite is needed for plotting SVGs"
+                              " (pip install svgwrite)")
+
+        scale, height, width, map_pt = self.compute_scale("px")
+
         assert filename is None or filename.endswith(".svg")
         img = svgwrite.Drawing(filename, size=(float(width), float(height)))
-
-        def map_pt(pt: PointType) -> typing.Tuple[float, float]:
-            """Map a point."""
-            return (
-                float(self.padding + (pt[0] - minx) * scale),
-                float(height - self.padding - (pt[1] - miny) * scale))
+        img.set_desc(title=self.title, desc=self.desc)
 
         for e in self.elements:
-            for f, args, kwargs in e.as_svg(map_pt):
-                img.add(getattr(img, f)(*args, **kwargs))
+            for func, args, kwargs in e.as_svg(map_pt):
+                img.add(getattr(img, func)(*args, **kwargs))
+
+        svg_string = img.tostring()
+
+        a, b = svg_string.split("<svg", 1)
+        c, d = b.split(">", 1)
+
+        svg_string = a + "<svg" + c + ">" + self.svg_metadata + d
 
         if filename is not None:
-            img.save()
+            with open(filename, "w") as f:
+                f.write(svg_string)
 
-        return img.tostring()
+        return svg_string
 
-    def as_png(self, filename: str):
+    def as_png(self, filename: str, png_scale: float = None, png_width: int = None,
+               png_height: int = None):
         """Convert to a PNG."""
         try:
             from cairosvg import svg2png
@@ -640,15 +872,49 @@ class Picture:
             raise ImportError("CairoSVG is needed for plotting PNGs"
                               " (pip install CairoSVG)")
 
+        if png_scale is not None:
+            assert png_width is None
+            assert png_height is None
+        elif png_width is not None:
+            assert png_height is None
+            png_scale = png_width / float(self.compute_scale("px")[2])
+        elif png_height is not None:
+            png_scale = png_height / float(self.compute_scale("px")[1])
+        else:
+            png_scale = 1.0
+
+        assert isinstance(png_scale, float)
         assert filename.endswith(".png")
-        svg2png(bytestring=self.as_svg(None), write_to=filename)
+        svg2png(bytestring=self.as_svg(None), write_to=filename, scale=png_scale)
+
+    def as_tikz(self, filename: str = None) -> str:
+        """Convert to tikz."""
+        scale, height, width, map_pt = self.compute_scale("cm", False)
+        tikz = self.tex_comment
+        tikz += "\\begin{tikzpicture}[x=1cm,y=1cm]\n"
+
+        inner_tikz = ""
+        for e in self.elements:
+            inner_tikz += e.as_tikz(map_pt)
+
+        tikz += colors.get_tikz_definitions() + inner_tikz
+
+        tikz += "\\end{tikzpicture}\n"
+
+        if filename is not None:
+            with open(filename, "w") as f:
+                f.write(tikz)
+
+        return tikz
 
-    def save(self, filename: str):
+    def save(self, filename: str, plot_options: typing.Dict[str, typing.Any] = {}):
         """Save the picture as a file."""
         if filename.endswith(".svg"):
-            self.as_svg(filename)
+            self.as_svg(filename, **plot_options)
         elif filename.endswith(".png"):
-            self.as_png(filename)
+            self.as_png(filename, **plot_options)
+        elif filename.endswith(".tex"):
+            self.as_tikz(filename, **plot_options)
         else:
             if "." in filename:
                 ext = "." + filename.split(".")[-1]
diff --git a/symfem/references.py b/symfem/references.py
index f04c8b1..206a97b 100644
--- a/symfem/references.py
+++ b/symfem/references.py
@@ -8,6 +8,11 @@ from .geometry import (PointType, PointTypeInput, SetOfPoints, SetOfPointsInput,
                        parse_set_of_points_input, parse_point_input)
 from .symbols import t, x, AxisVariablesNotSingle
 
+LatticeWithLines = typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]
+IntLimits = typing.List[typing.Union[
+    typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
+    typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr]]]
+
 
 def _vsub(v: PointTypeInput, w: PointTypeInput) -> PointType:
     """Subtract.
@@ -169,9 +174,7 @@ class Reference(ABC):
         pass
 
     @abstractmethod
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -216,10 +219,7 @@ class Reference(ABC):
         return tuple(pt)
 
     @abstractmethod
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -498,11 +498,13 @@ class Reference(ABC):
         """
         pass
 
-    def plot_entity_diagrams(self, filename: str, width: int = None, height: int = None):
+    def plot_entity_diagrams(
+        self, filename: str, plot_options: typing.Dict[str, typing.Any] = {}, **kwargs: typing.Any
+    ):
         """Plot diagrams showing the entity numbering of the reference."""
         from .plotting import Picture, colors
 
-        img = Picture(width=width, height=height)
+        img = Picture(**kwargs)
 
         if self.tdim == 1:
             offset_unit: typing.Tuple[typing.Union[
@@ -558,15 +560,13 @@ class Reference(ABC):
                         ref = self.sub_entity(dim, n)
                         img.add_ncircle(offset(ref.midpoint(), current_dim), n, colors.entity(dim))
 
-        img.save(filename)
+        img.save(filename, plot_options)
 
 
 class Point(Reference):
     """A point."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = (tuple(), )
-    ):
+    def __init__(self, vertices: SetOfPointsInput = ((), )):
         """Create a point.
 
         Args:
@@ -577,12 +577,12 @@ class Point(Reference):
             tdim=0,
             name="point",
             origin=vertices[0],
-            axes=tuple(),
-            reference_vertices=(tuple(), ),
+            axes=(),
+            reference_vertices=((), ),
             vertices=vertices,
-            edges=tuple(),
-            faces=tuple(),
-            volumes=tuple(),
+            edges=(),
+            faces=(),
+            volumes=(),
             sub_entity_types=["point", None, None, None],
             simplex=True, tp=True)
 
@@ -605,9 +605,7 @@ class Point(Reference):
         """
         raise NotImplementedError()
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -619,10 +617,7 @@ class Point(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -698,9 +693,7 @@ class Point(Reference):
 class Interval(Reference):
     """An interval."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0,), (1,))
-    ):
+    def __init__(self, vertices: SetOfPointsInput = ((0,), (1,))):
         """Create an interval.
 
         Args:
@@ -715,8 +708,8 @@ class Interval(Reference):
             reference_vertices=((0,), (1,)),
             vertices=vertices,
             edges=((0, 1),),
-            faces=tuple(),
-            volumes=tuple(),
+            faces=(),
+            volumes=(),
             sub_entity_types=["point", "interval", None, None],
             simplex=True, tp=True)
 
@@ -740,9 +733,7 @@ class Interval(Reference):
         assert self.vertices == self.reference_vertices
         return tuple((sympy.Rational(2 * i + 1, 2 * (n + 1)), ) for i in range(n))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -757,10 +748,7 @@ class Interval(Reference):
         pairs = [(i, i + 1) for i in range(n - 1)]
         return pts, pairs
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -841,9 +829,7 @@ class Interval(Reference):
 class Triangle(Reference):
     """A triangle."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0), (1, 0), (0, 1))
-    ):
+    def __init__(self, vertices: SetOfPointsInput = ((0, 0), (1, 0), (0, 1))):
         """Create a triangle.
 
         Args:
@@ -859,7 +845,7 @@ class Triangle(Reference):
             vertices=vertices,
             edges=((1, 2), (0, 2), (0, 1)),
             faces=((0, 1, 2),),
-            volumes=tuple(),
+            volumes=(),
             sub_entity_types=["point", "interval", "triangle", None],
             simplex=True)
 
@@ -885,9 +871,7 @@ class Triangle(Reference):
             for o, a0, a1 in zip(self.origin, *self.axes)
         ) for i in range(n) for j in range(n - i))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -924,10 +908,7 @@ class Triangle(Reference):
         """
         return [[(1, 0), (1, 1)], [(0, 2)], [(2, 0), (1, 2), (0, 0), (0, 1)]]
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -1022,9 +1003,7 @@ class Triangle(Reference):
 class Tetrahedron(Reference):
     """A tetrahedron."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1))
-    ):
+    def __init__(self, vertices: SetOfPointsInput = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1))):
         """Create a tetrahedron.
 
         Args:
@@ -1091,9 +1070,7 @@ class Tetrahedron(Reference):
             for o, a0, a1, a2 in zip(self.origin, *self.axes)
         ) for i in range(n) for j in range(n - i) for k in range(n - i - j))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -1105,10 +1082,7 @@ class Tetrahedron(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -1204,9 +1178,7 @@ class Tetrahedron(Reference):
 class Quadrilateral(Reference):
     """A quadrilateral."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0), (1, 0), (0, 1), (1, 1))
-    ):
+    def __init__(self, vertices: SetOfPointsInput = ((0, 0), (1, 0), (0, 1), (1, 1))):
         """Create a quadrilateral.
 
         Args:
@@ -1222,7 +1194,7 @@ class Quadrilateral(Reference):
             vertices=vertices,
             edges=((0, 1), (0, 2), (1, 3), (2, 3)),
             faces=((0, 1, 2, 3),),
-            volumes=tuple(),
+            volumes=(),
             sub_entity_types=["point", "interval", "quadrilateral", None],
             tp=True)
 
@@ -1257,9 +1229,7 @@ class Quadrilateral(Reference):
             for o, a0, a1 in zip(self.origin, *self.axes)
         ) for i in range(n + 1) for j in range(n + 1))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -1293,10 +1263,7 @@ class Quadrilateral(Reference):
         """
         return [[(1, 3), (1, 1), (0, 2)], [(2, 0)], [(1, 2), (0, 3), (1, 0), (0, 0), (0, 1)]]
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -1411,9 +1378,8 @@ class Quadrilateral(Reference):
 class Hexahedron(Reference):
     """A hexahedron."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0),
-                                            (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1))
+    def __init__(self, vertices: SetOfPointsInput = (
+        (0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0), (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1))
     ):
         """Create a hexahedron.
 
@@ -1492,9 +1458,7 @@ class Hexahedron(Reference):
             sympy.Rational(2 * k + 1, 2 * (n + 1))
         ) for i in range(n + 1) for j in range(n + 1) for k in range(n + 1))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -1506,10 +1470,7 @@ class Hexahedron(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -1619,9 +1580,8 @@ class Hexahedron(Reference):
 class Prism(Reference):
     """A (triangular) prism."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0, 0), (1, 0, 0), (0, 1, 0),
-                                            (0, 0, 1), (1, 0, 1), (0, 1, 1))
+    def __init__(self, vertices: SetOfPointsInput = (
+        (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (0, 1, 1))
     ):
         """Create a prism.
 
@@ -1703,9 +1663,7 @@ class Prism(Reference):
             sympy.Rational(2 * k + 1, 2 * (n + 1))
         ) for i in range(n + 1) for j in range(n + 1 - i) for k in range(n + 1))
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -1717,10 +1675,7 @@ class Prism(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -1827,9 +1782,8 @@ class Prism(Reference):
 class Pyramid(Reference):
     """A (square-based) pyramid."""
 
-    def __init__(
-        self, vertices: SetOfPointsInput = ((0, 0, 0), (1, 0, 0), (0, 1, 0),
-                                            (1, 1, 0), (0, 0, 1))
+    def __init__(self, vertices: SetOfPointsInput = (
+        (0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0), (0, 0, 1))
     ):
         """Create a pyramid.
 
@@ -1905,9 +1859,7 @@ class Pyramid(Reference):
         """
         raise NotImplementedError()
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:
@@ -1919,10 +1871,7 @@ class Pyramid(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -2035,10 +1984,7 @@ class DualPolygon(Reference):
 
     reference_origin: PointType
 
-    def __init__(
-        self, number_of_triangles: int,
-        vertices: SetOfPointsInput = None
-    ):
+    def __init__(self, number_of_triangles: int, vertices: SetOfPointsInput = None):
         """Create a dual polygon.
 
         Args:
@@ -2068,14 +2014,14 @@ class DualPolygon(Reference):
         super().__init__(
             tdim=2,
             name="dual polygon",
-            axes=tuple(),
+            axes=(),
             origin=origin,
             vertices=vertices,
             reference_vertices=tuple(reference_vertices),
             edges=tuple((i, (i + 1) % (2 * number_of_triangles))
                         for i in range(2 * number_of_triangles)),
             faces=(tuple(range(2 * number_of_triangles)), ),
-            volumes=tuple(),
+            volumes=(),
             sub_entity_types=["point", "interval", f"dual polygon({number_of_triangles})", None],
 
         )
@@ -2091,10 +2037,7 @@ class DualPolygon(Reference):
         """
         raise NotImplementedError()
 
-    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> typing.List[typing.Union[
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr, sympy.core.expr.Expr],
-        typing.Tuple[sympy.core.symbol.Symbol, sympy.core.expr.Expr],
-    ]]:
+    def integration_limits(self, vars: AxisVariablesNotSingle = t) -> IntLimits:
         """Get the limits for an integral over this reference.
 
         Args:
@@ -2170,15 +2113,13 @@ class DualPolygon(Reference):
         """
         assert self.vertices == self.reference_vertices
         from .create import create_reference
-        lattice: SetOfPoints = tuple()
+        lattice: SetOfPoints = ()
         for v1, v2 in zip(self.vertices, self.vertices[1:] + (self.vertices[0], )):
             ref = create_reference("triangle", (self.origin, v1, v2))
             lattice += ref.make_lattice(n // 2)
         return lattice
 
-    def make_lattice_with_lines(
-        self, n: int
-    ) -> typing.Tuple[SetOfPoints, typing.List[typing.Tuple[int, int]]]:
+    def make_lattice_with_lines(self, n: int) -> LatticeWithLines:
         """Make a lattice of points, and a list of lines connecting them.
 
         Args:


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index ed491cb..40c8e16 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -25,6 +25,10 @@ jobs:
       - uses: actions/checkout@v2
       - run: pip install .[test] pytest-xdist
         name: Install Symfem
+      - name: Install LaTeΧ
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y texlive-latex-base texlive-latex-extra
       - run: python3 -m pytest -n=auto --durations=50 test -W error
         name: Run unit tests
       - run: python3 -m pytest demo/test_demos.py -W error
diff --git a/test/test_plotting.py b/test/test_plotting.py
index 660da3e..0051551 100644
--- a/test/test_plotting.py
+++ b/test/test_plotting.py
@@ -6,30 +6,47 @@ import sympy
 import symfem
 import symfem.plotting
 
-dir = os.path.dirname(os.path.realpath(__file__))
+folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../_temp")
+os.system(f"mkdir {folder}")
+
+
+def compile_tex(filename):
+    with open(os.path.join(folder, filename)) as f:
+        content = f.read()
+    filename2 = filename.split(".")[0] + "-with-preamble.tex"
+    with open(os.path.join(folder, filename2), "w") as f:
+        f.write("\\documentclass{standalone}\n")
+        f.write("\\usepackage{tikz}\n")
+        f.write("\\begin{document}\n")
+        f.write(content)
+        f.write("\\end{document}\n")
+    assert os.system(f"cd {folder} && pdflatex -halt-on-error {filename2} > /dev/null") == 0
 
 
 def test_plot_line():
     p = symfem.plotting.Picture()
     p.add_line((sympy.Integer(0), sympy.Integer(0)),
                (sympy.Integer(1), sympy.Integer(1)), "blue")
-    for ext in ["svg", "png"]:
-        p.save(os.path.join(dir, f"test-output-test_plot_line.{ext}"))
+    for ext in ["svg", "png", "tex"]:
+        p.save(os.path.join(folder, f"test_plot_line.{ext}"))
+    compile_tex("test_plot_line.tex")
 
 
 def test_plot_arrow():
     p = symfem.plotting.Picture()
     p.add_arrow((sympy.Integer(0), sympy.Integer(0)),
                 (sympy.Integer(1), sympy.Integer(1)), "orange")
-    for ext in ["svg", "png"]:
-        p.save(os.path.join(dir, f"test-output-test_plot_arrow.{ext}"))
+    for ext in ["svg", "png", "tex"]:
+        p.save(os.path.join(folder, f"test_plot_arrow.{ext}"))
+    compile_tex("test_plot_arrow.tex")
 
 
 def test_plot_ncircle():
     p = symfem.plotting.Picture()
     p.add_ncircle((sympy.Integer(0), sympy.Integer(0)), 5, "green")
-    for ext in ["svg", "png"]:
-        p.save(os.path.join(dir, f"test-output-test_plot_ncircle.{ext}"))
+    for ext in ["svg", "png", "tex"]:
+        p.save(os.path.join(folder, f"test_plot_ncircle.{ext}"))
+    compile_tex("test_plot_ncircle.tex")
 
 
 def test_plot_fill():
@@ -38,8 +55,9 @@ def test_plot_fill():
         (sympy.Integer(0), sympy.Integer(0)), (sympy.Integer(1), sympy.Integer(1)),
         (sympy.Integer(1), sympy.Rational(1, 2)), (sympy.Rational(1, 2), sympy.Integer(0))
     ), "purple", 0.5)
-    for ext in ["svg", "png"]:
-        p.save(os.path.join(dir, f"test-output-test_plot_fill.{ext}"))
+    for ext in ["svg", "png", "tex"]:
+        p.save(os.path.join(folder, f"test_plot_fill.{ext}"))
+    compile_tex("test_plot_fill.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -66,9 +84,10 @@ def test_z_ordering(reference):
 @pytest.mark.parametrize("degree", [0, 2, 5])
 def test_dof_diagrams_lagrange(reference, degree):
     e = symfem.create_element(reference, "P", degree)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_dof_diagram(os.path.join(
-            dir, f"test-output-test_dof_diagrams_lagrange-{reference}-{degree}.{ext}"))
+            folder, f"test_dof_diagrams_lagrange-{reference}-{degree}.{ext}"))
+    compile_tex(f"test_dof_diagrams_lagrange-{reference}-{degree}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -81,9 +100,10 @@ def test_dof_diagrams_raviart_thomas(reference, degree):
         e = symfem.create_element(reference, "Qdiv", degree)
     else:
         e = symfem.create_element(reference, "RT", degree)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_dof_diagram(os.path.join(
-            dir, f"test-output-test_dof_diagrams_raviart_thomas-{reference}-{degree}.{ext}"))
+            folder, f"test_dof_diagrams_raviart_thomas-{reference}-{degree}.{ext}"))
+    compile_tex(f"test_dof_diagrams_raviart_thomas-{reference}-{degree}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -92,9 +112,10 @@ def test_dof_diagrams_raviart_thomas(reference, degree):
 @pytest.mark.parametrize("degree", [0, 1, 2])
 def test_function_plots_lagrange(reference, degree):
     e = symfem.create_element(reference, "P", degree)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_lagrange-{reference}-{degree}.{ext}"))
+            folder, f"test_function_plots_lagrange-{reference}-{degree}.{ext}"))
+    compile_tex(f"test_function_plots_lagrange-{reference}-{degree}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -107,9 +128,10 @@ def test_function_plots_raviart_thomas(reference, degree):
         e = symfem.create_element(reference, "Qdiv", degree)
     else:
         e = symfem.create_element(reference, "RT", degree)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_raviart_thomas-{reference}-{degree}.{ext}"))
+            folder, f"test_function_plots_raviart_thomas-{reference}-{degree}.{ext}"))
+    compile_tex(f"test_function_plots_raviart_thomas-{reference}-{degree}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -117,9 +139,10 @@ def test_function_plots_raviart_thomas(reference, degree):
 ])
 def test_function_plots_piecewise_scalar(reference):
     e = symfem.create_element(reference, "P1-iso-P2", 1)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_piecewise_scalar-{reference}.{ext}"))
+            folder, f"test_function_plots_piecewise_scalar-{reference}.{ext}"))
+    compile_tex(f"test_function_plots_piecewise_scalar-{reference}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -127,25 +150,28 @@ def test_function_plots_piecewise_scalar(reference):
 ])
 def test_function_plots_piecewise_vector(reference):
     e = symfem.create_element(reference, "Guzman-Neilan", 1)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_piecewise_vector-{reference}.{ext}"))
+            folder, f"test_function_plots_piecewise_vector-{reference}.{ext}"))
+    compile_tex(f"test_function_plots_piecewise_vector-{reference}.tex")
 
 
 @pytest.mark.parametrize("n", [4, 6])
 def test_function_plots_dual(n):
     e = symfem.create_element(f"dual polygon({n})", "dual", 1)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_dual-{n}.{ext}"))
+            folder, f"test_function_plots_dual-{n}.{ext}"))
+    compile_tex(f"test_function_plots_dual-{n}.tex")
 
 
 @pytest.mark.parametrize("n", [4, 6])
 def test_function_plots_bc(n):
     e = symfem.create_element(f"dual polygon({n})", "BC", 1)
-    for ext in ["svg", "png"]:
+    for ext in ["svg", "png", "tex"]:
         e.plot_basis_function(0, os.path.join(
-            dir, f"test-output-test_function_plots_bc-{n}.{ext}"))
+            folder, f"test_function_plots_bc-{n}.{ext}"))
+    compile_tex(f"test_function_plots_bc-{n}.tex")
 
 
 @pytest.mark.parametrize("reference", [
@@ -155,6 +181,39 @@ def test_function_plots_bc(n):
 ])
 def test_plot_reference(reference):
     r = symfem.create_reference(reference)
-    for ext in ["svg", "png"]:
+    rname = reference.replace("(", "").replace(")", "").replace(" ", "_")
+    for ext in ["svg", "png", "tex"]:
         r.plot_entity_diagrams(os.path.join(
-            dir, f"test-output-test_plot_references-{reference}.{ext}"))
+            folder, f"test_plot_reference-{rname}.{ext}"))
+    compile_tex(f"test_plot_reference-{rname}.tex")
+
+
+def test_metadata():
+    img = symfem.plotting.Picture(svg_metadata=(
+        "<metadata id='license'>\n"
+        " <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' "
+        "xmlns:dc='http://purl.org/dc/elements/1.1/' "
+        "xmlns:cc='http://web.resource.org/cc/'>\n"
+        "   <cc:Work rdf:about=''>\n"
+        "     <dc:title>Title</dc:title>\n"
+        "     <dc:date>1970-01-01</dc:date>\n"
+        "     <dc:creator>\n"
+        "       <cc:Agent><dc:title>Symfem</dc:title></cc:Agent>\n"
+        "     </dc:creator>\n"
+        "     <dc:description>See document description</dc:description>\n"
+        "     <cc:license rdf:resource='http://creativecommons.org/licenses/by/4.0/'/>\n"
+        "     <dc:format>image/svg+xml</dc:format>\n"
+        "     <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>\n"
+        "   </cc:Work>\n"
+        "   <cc:License rdf:about='http://creativecommons.org/licenses/by/4.0/'>\n"
+        "     <cc:permits rdf:resource='http://web.resource.org/cc/Reproduction'/>\n"
+        "     <cc:permits rdf:resource='http://web.resource.org/cc/Distribution'/>\n"
+        "     <cc:permits rdf:resource='http://web.resource.org/cc/DerivativeWorks'/>\n"
+        "     <cc:requires rdf:resource='http://web.resource.org/cc/Notice'/>\n"
+        "     <cc:requires rdf:resource='http://web.resource.org/cc/Attribution'/>\n"
+        "   </cc:License>\n"
+        " </rdf:RDF>\n"
+        "</metadata>\n"))
+    img.add_line((sympy.Integer(0), sympy.Integer(0)),
+                 (sympy.Integer(1), sympy.Integer(1)), symfem.plotting.colors.ORANGE)
+    img.save(os.path.join(folder, "test_metadata.svg"))