├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── docs ├── atan_sin.PNG ├── gnuplot.PNG ├── perf.PNG ├── rand.PNG └── sine.PNG ├── examples ├── atan_sin.exs ├── atan_sin_dataset.exs ├── perf.exs ├── polynomial.exs ├── rand.exs ├── sine.exs ├── so327576.exs └── stress.exs ├── lib ├── gnuplot.ex └── gnuplot │ ├── bin.ex │ ├── commands.ex │ └── dataset.ex ├── mix.exs ├── mix.lock └── test ├── gnuplot_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterFilter, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 137 | 138 | # 139 | ## Warnings 140 | # 141 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 142 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 143 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 144 | {Credo.Check.Warning.IExPry, []}, 145 | {Credo.Check.Warning.IoInspect, []}, 146 | {Credo.Check.Warning.OperationOnSameValues, []}, 147 | {Credo.Check.Warning.OperationWithConstantResult, []}, 148 | {Credo.Check.Warning.RaiseInsideRescue, []}, 149 | {Credo.Check.Warning.SpecWithStruct, []}, 150 | {Credo.Check.Warning.WrongTestFileExtension, []}, 151 | {Credo.Check.Warning.UnusedEnumOperation, []}, 152 | {Credo.Check.Warning.UnusedFileOperation, []}, 153 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 154 | {Credo.Check.Warning.UnusedListOperation, []}, 155 | {Credo.Check.Warning.UnusedPathOperation, []}, 156 | {Credo.Check.Warning.UnusedRegexOperation, []}, 157 | {Credo.Check.Warning.UnusedStringOperation, []}, 158 | {Credo.Check.Warning.UnusedTupleOperation, []}, 159 | {Credo.Check.Warning.UnsafeExec, []} 160 | ], 161 | disabled: [ 162 | # 163 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 164 | 165 | # 166 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 167 | # and be sure to use `mix credo --strict` to see low priority checks) 168 | # 169 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 170 | {Credo.Check.Consistency.UnusedVariableNames, []}, 171 | {Credo.Check.Design.DuplicatedCode, []}, 172 | {Credo.Check.Design.SkipTestWithoutComment, []}, 173 | {Credo.Check.Readability.AliasAs, []}, 174 | {Credo.Check.Readability.BlockPipe, []}, 175 | {Credo.Check.Readability.ImplTrue, []}, 176 | {Credo.Check.Readability.MultiAlias, []}, 177 | {Credo.Check.Readability.NestedFunctionCalls, []}, 178 | {Credo.Check.Readability.SeparateAliasRequire, []}, 179 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 180 | {Credo.Check.Readability.SinglePipe, []}, 181 | {Credo.Check.Readability.Specs, []}, 182 | {Credo.Check.Readability.StrictModuleLayout, []}, 183 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 184 | {Credo.Check.Refactor.ABCSize, []}, 185 | {Credo.Check.Refactor.AppendSingleItem, []}, 186 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 187 | {Credo.Check.Refactor.FilterReject, []}, 188 | {Credo.Check.Refactor.IoPuts, []}, 189 | {Credo.Check.Refactor.MapMap, []}, 190 | {Credo.Check.Refactor.ModuleDependencies, []}, 191 | {Credo.Check.Refactor.NegatedIsNil, []}, 192 | {Credo.Check.Refactor.PipeChainStart, []}, 193 | {Credo.Check.Refactor.RejectFilter, []}, 194 | {Credo.Check.Refactor.VariableRebinding, []}, 195 | {Credo.Check.Warning.LazyLogging, []}, 196 | {Credo.Check.Warning.LeakyEnvironment, []}, 197 | {Credo.Check.Warning.MapGetUnsafePass, []}, 198 | {Credo.Check.Warning.MixEnv, []}, 199 | {Credo.Check.Warning.UnsafeToAtom, []} 200 | 201 | # {Credo.Check.Refactor.MapInto, []}, 202 | 203 | # 204 | # Custom checks can be created using `mix credo.gen.check`. 205 | # 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Elixir ${{matrix.elixir}} (Erlang/OTP ${{matrix.otp}}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | otp: ["23.3.4", "24.3.4"] 12 | elixir: ["1.14", "1.13", "1.12", "1.11.4", "1.10.4"] 13 | exclude: 14 | - elixir: "1.10.4" 15 | otp: "24.3.4" 16 | include: 17 | - elixir: "1.14" 18 | otp: "25.1" 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: erlef/setup-beam@v1 22 | with: 23 | otp-version: ${{matrix.otp}} 24 | elixir-version: ${{matrix.elixir}} 25 | - run: mix deps.get 26 | - run: mix compile --warnings-as-errors 27 | - run: mix credo --strict 28 | - name: "Check formatted?" 29 | run: mix format mix.exs "examples/*.exs" "lib/**/*.{ex,exs}" "test/**/*.exs" --check-formatted 30 | if: ${{ startsWith(matrix.elixir, '1.14') }} 31 | - run: mix test --exclude gnuplot:true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | gnuplot-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gnuplot Elixir 2 | 3 | A simple interface from [Elixir data][7] to the [Gnuplot graphing utility][1] that uses [Erlang Ports][5] to transmit data from your application to Gnuplot. Datasets are streamed directly to STDIN without temporary files and you can plot [1M points in 12.7 seconds](examples/stress.exs). 4 | 5 | Please visit the [Gnuplot demos gallery](http://gnuplot.sourceforge.net/demo_5.3/) to see all the possibilities, the [manual which describes the grammar](http://www.gnuplot.info/docs_5.2/Gnuplot_5.2.pdf), and the [examples folder](examples/). 6 | 7 | This is a conversion of the [Clojure Gnuplot library][4] by [aphyr][2]. This library has been tested on OS X, Ubuntu 16.04 and CentOS 7.6. 8 | 9 | [![Build Status](https://github.com/devstopfix/gnuplot-elixir/workflows/ci/badge.svg)](https://github.com/devstopfix/gnuplot-elixir/actions) 10 | [![Hex.pm](https://img.shields.io/hexpm/v/gnuplot.svg?style=flat-square)](https://hex.pm/packages/gnuplot) 11 | [![API Docs](https://img.shields.io/badge/api-docs-MediumPurple.svg?style=flat)](https://hexdocs.pm/gnuplot/Gnuplot.html) 12 | ![Platforms](https://img.shields.io/badge/platform-osx%7Cubuntu%7Ccentos-black.svg) 13 | 14 | ## Usage 15 | 16 | The `plot` function takes two arguments: 17 | 18 | * a list of commands (each of which is a list of terms) 19 | * a list of Streams or Enumerable datasets (not required when plotting functions) 20 | 21 | Commands are lists of terms that normally start with an atom such as `:set`. They may be written as lists or [Word lists](https://elixir-lang.org/getting-started/sigils.html#word-lists) - the following lines are equivalent: 22 | 23 | * `[:set, :xtics, :off]` 24 | * `~w(set xtics off)a` 25 | 26 | and both convert to `set xtics off`. 27 | 28 | Strings are output inside double quotes, and charlists are output without modification. `[:plot, 'sin(x)', :title, "Sine Wave"]` becomes: `plot sin(x) title "Sine Wave"` 29 | 30 | A dataset is a list of points, each point is a list of numbers. A dataset can be a Stream. 31 | 32 | ### Scatter plot with a single dataset 33 | 34 | Lets compare the distributions of the [Erlang rand functions](http://erlang.org/doc/man/rand.html): 35 | 36 | ```elixir 37 | dataset = for _ <- 0..1000, do: [:rand.uniform(), :rand.normal()] 38 | {:ok, _cmd} = Gnuplot.plot([ 39 | [:set, :title, "rand uniform vs normal"], 40 | [:plot, "-", :with, :points] 41 | ], [dataset]) 42 | ``` 43 | 44 | Gnuplot will by default open a window containing your plot: 45 | 46 | ![rand](docs/gnuplot.PNG) 47 | 48 | The command string sent (`_cmd` above) can be manually inspected should the chart not appear as you expected. If the chart is not drawn due to an error then the result will be `{:error, cmd, errors}`. 49 | 50 | ### PNG of two datasets 51 | 52 | Write two datasets to a PNG file: 53 | 54 | ```elixir 55 | import Gnuplot 56 | 57 | {:ok, _cmd} = plot([ 58 | [:set, :term, :pngcairo], 59 | [:set, :output, "/tmp/rand.png"], 60 | [:set, :title, "rand uniform vs normal"], 61 | [:set, :key, :left, :top], 62 | plots([ 63 | ["-", :title, "uniform", :with, :points], 64 | ["-", :title, "normal", :with, :points] 65 | ]) 66 | ], 67 | [ 68 | for(n <- 0..100, do: [n, n * :rand.uniform()]), 69 | for(n <- 0..100, do: [n, n * :rand.normal()]) 70 | ]) 71 | ``` 72 | 73 | ![uniform-vs-rand](docs/rand.PNG) 74 | 75 | When we are plotting multiple datasets in the same chart we need a comma separated list for the `plot` command which is made with the `plots`, `splots` or `list` function. 76 | 77 | NB the `:png` terminal can also be used but it produces [rougher output](http://www.gnuplotting.org/output-terminals/). 78 | 79 | 80 | ### Plot functions without datasets 81 | 82 | ```elixir 83 | Gnuplot.plot([[:plot, 'sin(x)', :title, "Sine Wave"]]) 84 | ``` 85 | 86 | ![rand](docs/sine.PNG) 87 | 88 | ```elixir 89 | Gnuplot.plot([ 90 | ~w(set autoscale)a, 91 | ~w(set samples 800)a, 92 | [:plot, -30..20, 'sin(x*20)*atan(x)'] 93 | ]) 94 | ``` 95 | 96 | NB [ranges](https://hexdocs.pm/elixir/Range.html) can be used 97 | 98 | ![rand](docs/atan_sin.PNG) 99 | 100 | ### Multiplot 101 | 102 | The `multiplot` mode places serveral plots on the same page: 103 | 104 | ```elixir 105 | Gnuplot.plot([ 106 | [:set, :multiplot, :layout, '2,1'], 107 | [:plot, 'sin(x)/x'], 108 | [:plot, 'cos(x)'] 109 | ]) 110 | ``` 111 | 112 | ## Installation 113 | 114 | This library is [available in Hex](https://hex.pm/packages/gnuplot) with [documentation](https://hexdocs.pm/gnuplot/Gnuplot.html) and the package can be installed by adding `gnuplot` to your project: 115 | 116 | ```elixir 117 | def deps do 118 | [ 119 | {:gnuplot, "~> 1.22"} 120 | ] 121 | end 122 | ``` 123 | 124 | ## Testing 125 | 126 | Some tests create plots which require `gnuplot` to be installed. They can be be excluded with: 127 | 128 | mix test --exclude gnuplot:true 129 | 130 | ## Performance 131 | 132 | The performance of the library on a MacBook Air is comparable to the Clojure version when `gnuplot` draws to a GUI. It is a little faster when writing directly to a PNG when running on a server. The times below are in milliseconds. Each plot was made in increasing order of the number of points and after a cold start of the VM. The last two columns show the refactoring from Enumerable to Streams. 133 | 134 | | Points | Clojure GUI | Elixir GUI | Elixir PNG | Elixir Enum | Elixir Stream | 135 | | -----: | ----------: | ---------: | ---------: | ------------: | ------------: | 136 | | 1 | 1,487 | 5 | 18 | 4 | 5 | 137 | | 10 | 1,397 | 10 | 1 | <1 | 1 | 138 | | 1e2 | 1,400 | 4 | 12 | 1 | 1 | 139 | | 1e3 | 1,381 | 59 | 52 | 8 | 10 | 140 | | 1e4 | 1,440 | 939 | 348 | 211 | 211 | 141 | | 1e5 | 5,784 | 5,801 | 3,494 | 1,873 | 1,313 | 142 | | 1e6 | 49,275 | 43,464 | 35,505 | 19,916 | 12,775 | 143 | | | MacBook | MacBook | MacBook | Ubuntu 16.04 | Ubuntu 16.04 | 144 | | | 2.5 GHz i5 | 2.5 GHz i5 | 2.5 GHz i5 | 3.3 GHz 2vCPU | 3.3 GHz 2vCPU | 145 | 146 | ![performance](docs/perf.PNG) 147 | 148 | ```elixir 149 | points = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000] 150 | clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275] 151 | elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464] 152 | elixir_png = [0.002, 0.010, 0.049, 0.040, 0.349, 4.091, 41.521] 153 | ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916] 154 | ubuntu_strm = [0.002, 0.001, 0.001, 0.009, 0.204, 1.279, 12.858] 155 | datasets = for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m, ubuntu_strm], do: 156 | Enum.zip(points, ds) 157 | 158 | Gnuplot.plot([ 159 | [:set, :title, "Time to render scatter plots"], 160 | [:set, :xlabel, "Points in plot"], 161 | [:set, :ylabel, "Elapsed (s)"], 162 | ~w(set key left top)a, 163 | ~w(set logscale xy)a, 164 | ~w(set grid xtics ytics)a, 165 | ~w(set style line 1 lw 2 lc '#63b132')a, 166 | ~w(set style line 2 lw 2 lc '#2C001E')a, 167 | ~w(set style line 3 lw 2 lc '#5E2750')a, 168 | ~w(set style line 4 lw 2 lc '#E95420')a, 169 | ~w(set style line 5 lw 4 lc '#77216F')a, 170 | Gnuplot.plots([ 171 | ["-", :title, "Clojure GUI", :with, :lines, :ls, 1], 172 | ["-", :title, "Elixir GUI", :with, :lines, :ls, 2], 173 | ["-", :title, "Elixir PNG", :with, :lines, :ls, 3], 174 | ["-", :title, "Elixir t2.m", :with, :lines, :ls, 4], 175 | ["-", :title, "Elixir Stream", :with, :lines, :ls, 5] 176 | ])], 177 | datasets 178 | ) 179 | ``` 180 | 181 | ## Credits and licence 182 | 183 | Original design ©2015 [Kyle Kingsbury][2]. 184 | 185 | Elixir code ©2022 [DEVSTOPFIX LTD][3]. Contributions from [piisgaaf](https://github.com/piisgaaf) 186 | 187 | Distributed under the [Eclipse Public License v2][6]. 188 | 189 | 190 | [1]: http://www.gnuplot.info/ 191 | [2]: https://github.com/aphyr 192 | [3]: http://www.devstopfix.com/ 193 | [4]: https://github.com/aphyr/gnuplot 194 | [5]: http://erlang.org/doc/reference_manual/ports.html 195 | [6]: https://www.eclipse.org/legal/epl-2.0/ 196 | [7]: https://elixir-lang.org/getting-started/basic-types.html 197 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :gnuplot, timeout: {10_000, :ms} 4 | -------------------------------------------------------------------------------- /docs/atan_sin.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstopfix/gnuplot-elixir/a49bf15982e5821047f30bda0573a54d034b9317/docs/atan_sin.PNG -------------------------------------------------------------------------------- /docs/gnuplot.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstopfix/gnuplot-elixir/a49bf15982e5821047f30bda0573a54d034b9317/docs/gnuplot.PNG -------------------------------------------------------------------------------- /docs/perf.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstopfix/gnuplot-elixir/a49bf15982e5821047f30bda0573a54d034b9317/docs/perf.PNG -------------------------------------------------------------------------------- /docs/rand.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstopfix/gnuplot-elixir/a49bf15982e5821047f30bda0573a54d034b9317/docs/rand.PNG -------------------------------------------------------------------------------- /docs/sine.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstopfix/gnuplot-elixir/a49bf15982e5821047f30bda0573a54d034b9317/docs/sine.PNG -------------------------------------------------------------------------------- /examples/atan_sin.exs: -------------------------------------------------------------------------------- 1 | defmodule AtanSin do 2 | import Gnuplot 3 | 4 | @moduledoc "http://gnuplot.sourceforge.net/demo/simple.7.gnu" 5 | 6 | def png, do: Path.join("docs", "atan_sin.PNG") 7 | 8 | def target, 9 | do: [ 10 | [:set, :term, :pngcairo, :size, '512,256', :font, "Fira Sans"], 11 | [:set, :output, png()] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | ~w(set style line 1 linecolor rgb '#77216F' lw 1)a, 17 | ~w(set autoscale)a, 18 | ~w(set samples 800)a, 19 | [ 20 | :plot, 21 | -30..20, 22 | 'sin(x*20)*atan(x)', 23 | :ls, 24 | 1 25 | ] 26 | ] 27 | 28 | def plot, do: plot(target() ++ commands()) 29 | end 30 | 31 | # mix run examples/atan_sin.exs 32 | AtanSin.plot() 33 | -------------------------------------------------------------------------------- /examples/atan_sin_dataset.exs: -------------------------------------------------------------------------------- 1 | defmodule AtanSin do 2 | import Gnuplot 3 | 4 | @moduledoc "http://gnuplot.sourceforge.net/demo/simple.7.gnu" 5 | 6 | def png, do: Path.join("docs", "atan_sin_dataset.PNG") 7 | 8 | def target, 9 | do: [ 10 | [:set, :term, :png, :size, '512,256', :font, "/Library/Fonts/FiraCode-Medium.ttf", 12], 11 | [:set, :output, png()] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | [ 17 | :plot, 18 | "-", 19 | :with, 20 | :lines, 21 | :title, 22 | "sin(x*20)*atan(x)" 23 | ] 24 | ] 25 | 26 | def plot, do: plot(target() ++ commands(), [dataset()]) 27 | 28 | defp dataset do 29 | Enum.map(ffor(-30, 20, 800), fn x -> [x, :math.sin(x * 20) * :math.atan(x)] end) 30 | end 31 | 32 | # defp dataset2 do 33 | # for x <- -30_000..20_000, 34 | # do: [x / 1000.0, :math.sin(x * 20 / 1000.0) * :math.atan(x / 1000.0)] 35 | # end 36 | 37 | defp ffor(min, max, step), do: for(x <- min..max, dx <- 0..step, do: x + dx / step) 38 | end 39 | 40 | # mix run examples/atan_sin_dataset.exs 41 | AtanSin.plot() 42 | -------------------------------------------------------------------------------- /examples/perf.exs: -------------------------------------------------------------------------------- 1 | defmodule Perf do 2 | import Gnuplot 3 | 4 | @moduledoc false 5 | 6 | def png, do: Path.join("docs", "perf.PNG") 7 | 8 | def target, 9 | do: [ 10 | [:set, :term, :pngcairo, :size, '640,512', :font, "Source Code Pro,12"], 11 | [:set, :output, png()] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | [:set, :title, "Time to render scatter plots"], 17 | [:set, :xlabel, "Points in plot"], 18 | [:set, :ylabel, "Elapsed (s)"], 19 | ~w(set key left top)a, 20 | ~w(set logscale xy)a, 21 | ~w(set grid xtics ytics)a, 22 | ~w(set style line 1 lw 2 lc '#63b132')a, 23 | ~w(set style line 2 lw 2 lc '#2C001E')a, 24 | ~w(set style line 3 lw 2 lc '#5E2750')a, 25 | ~w(set style line 4 lw 2 lc '#E95420')a, 26 | ~w(set style line 5 lw 4 lc '#77216F')a, 27 | plots([ 28 | ["-", :title, "Clojure GUI", :with, :lines, :ls, 1], 29 | ["-", :title, "Elixir GUI", :with, :lines, :ls, 2], 30 | ["-", :title, "Elixir PNG", :with, :lines, :ls, 3], 31 | ["-", :title, "Elixir t2.m", :with, :lines, :ls, 4], 32 | ["-", :title, "Elixir Stream", :with, :lines, :ls, 5] 33 | ]) 34 | ] 35 | 36 | def data do 37 | points = [1, 10, 100, 1000, 10_000, 100_000, 1_000_000] 38 | clojure_gui = [1.487, 1.397, 1.400, 1.381, 1.440, 5.784, 49.275] 39 | elixir_gui = [0.005, 0.010, 0.004, 0.059, 0.939, 5.801, 43.464] 40 | elixir_png = [0.018, 0.001, 0.012, 0.052, 0.348, 3.494, 35.505] 41 | ubuntu_t2m = [0.004, 0.002, 0.001, 0.008, 0.211, 1.873, 19.916] 42 | ubuntu_stream = [0.002, 0.001, 0.001, 0.009, 0.204, 1.279, 12.858] 43 | 44 | for ds <- [clojure_gui, elixir_gui, elixir_png, ubuntu_t2m, ubuntu_stream], 45 | do: Enum.zip(points, ds) 46 | end 47 | 48 | def draw, do: plot(target() ++ commands(), data()) 49 | end 50 | 51 | # mix run examples/perf.exs 52 | {:ok, _} = Perf.draw() 53 | -------------------------------------------------------------------------------- /examples/polynomial.exs: -------------------------------------------------------------------------------- 1 | defmodule Polynomial do 2 | @moduledoc """ 3 | Polynomials graphs from Wikipedia. 4 | 5 | https://en.wikipedia.org/wiki/Polynomial#Graphs 6 | """ 7 | 8 | def png, do: Path.join("/tmp/", "polynomial.PNG") 9 | 10 | def target, 11 | do: [ 12 | [:set, :term, :pngcairo, :size, '400,400', :font, "Times,14"], 13 | [:set, :output, png()] 14 | ] 15 | 16 | def style, 17 | do: [ 18 | ~w(set style line 1 lw 3 lc '#DD0000')a, 19 | ~w(set style line 2 lw 3 lc '#000000')a, 20 | ~w(set style line 3 lw 1 lc '#CECECE')a, 21 | ~w(set xzeroaxis ls 2)a, 22 | ~w(set yzeroaxis ls 2)a, 23 | ~w(set grid ls 3)a, 24 | [:set, :xrange, -5..4], 25 | [:set, :yrange, -4..6], 26 | [:set, :format, :x, ""], 27 | [:set, :format, :y, ""], 28 | ~w(set border ls 3)a 29 | ] 30 | 31 | def commands, 32 | do: [ 33 | [:set, :title, "Wikipedia Polynomial"], 34 | [ 35 | :plot, 36 | '((x**3)/4)+(3*(x**2)/4)-(3*x/2)-2', 37 | :ls, 38 | 1, 39 | :notitle 40 | ] 41 | ] 42 | 43 | def plot, do: {:ok, _} = Gnuplot.plot(target() ++ style() ++ commands()) 44 | end 45 | 46 | # mix run examples/polynomial.exs 47 | Polynomial.plot() 48 | -------------------------------------------------------------------------------- /examples/rand.exs: -------------------------------------------------------------------------------- 1 | defmodule Rand do 2 | import Gnuplot 3 | 4 | @moduledoc false 5 | 6 | def png, do: Path.join("docs", "rand.PNG") 7 | 8 | def target, 9 | do: [ 10 | [:set, :term, :pngcairo, :size, '512,256', :font, "Fira Sans,12"], 11 | [:set, :output, png()] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | ~w(set key left top)a, 17 | ~w(set style line 1 lc rgb '#77216F' pt 13)a, 18 | ~w(set style line 2 lc rgb '#599B2B' pt 2)a, 19 | plots([ 20 | ["-", :title, "uniform", :with, :points, :ls, 1], 21 | ["-", :title, "normal", :with, :points, :ls, 2] 22 | ]) 23 | ] 24 | 25 | def data, 26 | do: [ 27 | for(n <- 0..99, do: [n, n * :rand.uniform()]), 28 | for(n <- 0..99, do: [n, n * :rand.normal()]) 29 | ] 30 | 31 | def draw, do: plot(target() ++ commands(), data()) 32 | end 33 | 34 | # mix run examples/rand.exs 35 | {:ok, _} = Rand.draw() 36 | -------------------------------------------------------------------------------- /examples/sine.exs: -------------------------------------------------------------------------------- 1 | defmodule Sine do 2 | import Gnuplot 3 | 4 | @moduledoc "http://gnuplot.sourceforge.net/demo/simple.7.gnu" 5 | 6 | def png, do: Path.join("docs", "sine.PNG") 7 | 8 | def target, 9 | do: [ 10 | [:set, :term, :pngcairo, :size, '512,256', :font, "Fira Sans"], 11 | [:set, :output, png()] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | ~w(set style line 1 linecolor rgb '#77216F' linetype 1 linewidth 2)a, 17 | ~w(unset xzeroaxis)a, 18 | ~w(unset yzeroaxis)a, 19 | ~w(set ytics -1,1)a, 20 | [ 21 | :plot, 22 | 'sin(x)', 23 | :title, 24 | "Sine Wave", 25 | :ls, 26 | 1 27 | ] 28 | ] 29 | 30 | def plot, do: plot(target() ++ commands()) 31 | end 32 | 33 | # mix run examples/sine.exs 34 | Sine.plot() 35 | -------------------------------------------------------------------------------- /examples/so327576.exs: -------------------------------------------------------------------------------- 1 | defmodule BarChart do 2 | import Gnuplot 3 | 4 | @moduledoc "Chart from https://stackoverflow.com/a/11551808/3366" 5 | 6 | def run do 7 | chart = [ 8 | [:set, :term, :png, :size, '512,512'], 9 | [:set, :output, Path.join("/tmp", "barchart.PNG")], 10 | [:set, :boxwidth, 0.5], 11 | ~w(set style fill solid)a, 12 | [:plot, "-", :using, '1:3:xtic(2)', :with, :boxes] 13 | ] 14 | 15 | dataset = [[0, "label", 100], [1, "label2", 450], [2, "bar label", 75]] 16 | 17 | plot(chart, [dataset]) 18 | end 19 | end 20 | 21 | # mix run examples/so327576.exs && open /tmp/barchart.PNG 22 | BarChart.run() 23 | -------------------------------------------------------------------------------- /examples/stress.exs: -------------------------------------------------------------------------------- 1 | defmodule Stress do 2 | alias Gnuplot, as: G 3 | 4 | @moduledoc "https://github.com/aphyr/gnuplot/blob/master/test/gnuplot/core_test.clj#L24" 5 | 6 | def png(n), do: Path.join("/tmp", "stress" <> to_string(n) <> ".PNG") 7 | 8 | def target(n), 9 | do: [ 10 | [:set, :term, :png, :size, '2048,1920', :font, "/Library/Fonts/FiraCode-Medium.ttf", 12], 11 | [:set, :output, png(n)] 12 | ] 13 | 14 | def commands, 15 | do: [ 16 | [:set, :title, "Noise plot"], 17 | ~w(set key left top)a, 18 | [:plot, "-", :with, :points] 19 | ] 20 | 21 | def data(n), 22 | do: 23 | Stream.unfold(0, fn i -> 24 | if i <= n do 25 | {[i / n, i * :rand.uniform()], i + 1} 26 | else 27 | nil 28 | end 29 | end) 30 | 31 | def plot(n), do: G.plot(target(n) ++ commands(), [data(n)]) 32 | end 33 | 34 | # time mix run examples/stress.exs 35 | 36 | for n <- [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000] do 37 | {t, _} = :timer.tc(fn -> Stress.plot(n) end) 38 | IO.inspect([n, Float.round(t / 1000.0 / 1000.0, 3)]) 39 | end 40 | -------------------------------------------------------------------------------- /lib/gnuplot.ex: -------------------------------------------------------------------------------- 1 | defmodule Gnuplot do 2 | @moduledoc """ 3 | Interface to the Gnuplot graphing library. 4 | 5 | Plot a sine function where Gnuplot generates the samples: 6 | 7 | Gnuplot.plot([ 8 | ~w(set autoscale)a, 9 | ~w(set samples 800)a, 10 | [:plot, -30..20, 'sin(x*20)*atan(x)'] 11 | ]) 12 | 13 | Plot a sine function where your program generates the data: 14 | 15 | Gnuplot.plot([ 16 | [:plot, "-", :with, :lines :title, "sin(x*20)*atan(x)"] 17 | ], 18 | [ 19 | for x <- -30_000..20_000, do: [x / 1000.0 , :math.sin(x * 20 / 1000.0) * :math.atan(x / 1000.0) ] 20 | ] 21 | ) 22 | 23 | """ 24 | 25 | alias Gnuplot.Commands 26 | import Gnuplot.Dataset 27 | import Gnuplot.Bin 28 | 29 | @type command_term :: atom() | charlist() | number() | Range.t() | String.t() 30 | 31 | @type command :: nonempty_list(command_term()) 32 | 33 | @timeout Application.compile_env(:gnuplot, :timeout, {10_000, :ms}) 34 | 35 | @doc """ 36 | Transmit commands without dataset. 37 | """ 38 | @spec plot(list(command())) :: 39 | {:ok, String.t()} | {:error, String.t(), list(String.t())} | :timeout 40 | def plot(commands), do: plot(commands, []) 41 | 42 | @doc """ 43 | Transmit commands and datasets to Gnuplot. 44 | 45 | ## Examples 46 | 47 | iex> Gnuplot.plot([[:plot, "-", :with, :lines]], [[[0, 0], [1, 2], [2, 4]]]) 48 | {:ok, "plot \"-\" with lines"} 49 | 50 | """ 51 | @spec plot(list(command()), list(Dataset.t())) :: 52 | {:ok, String.t()} | {:error, String.t(), list(String.t())} | :timeout 53 | def plot(commands, datasets) do 54 | {:ok, path} = gnuplot_bin() 55 | cmd = Commands.format(commands) 56 | args = ["-p", "-e", cmd] 57 | 58 | port = 59 | Port.open({:spawn_executable, path}, [:binary, :exit_status, :stderr_to_stdout, args: args]) 60 | 61 | transmit(port, datasets) 62 | loop(port, cmd) 63 | end 64 | 65 | defp loop(port, cmd, output \\ []) do 66 | result = 67 | receive do 68 | {_, {:data, message}} -> loop(port, cmd, [message | output]) 69 | {_, {:exit_status, 0}} -> {:ok, cmd} 70 | {_, {:exit_status, _}} -> {:error, cmd, Enum.reverse(output)} 71 | after 72 | timeout() -> :timeout 73 | end 74 | 75 | {_, :close} = send(port, {self(), :close}) 76 | result 77 | end 78 | 79 | defp timeout do 80 | case @timeout do 81 | {t, :ms} -> t 82 | t when is_integer(t) -> t 83 | end 84 | end 85 | 86 | @spec transmit(port(), list(Dataset.t())) :: :ok 87 | defp transmit(port, datasets) do 88 | :ok = 89 | datasets 90 | |> format_datasets() 91 | |> Stream.each(fn row -> send(port, {self(), {:command, row}}) end) 92 | |> Stream.run() 93 | end 94 | 95 | @doc """ 96 | Builds a comma separated list of plot commands that are overlayed in a single plot. 97 | 98 | Only useful inside `plot/1`: 99 | 100 | import Gnuplot 101 | 102 | plot([ 103 | [:set, :title, "Sine vs Cosine"], 104 | plots([ 105 | ['sin(x)'], 106 | ['cos(x)'] 107 | ]) 108 | ]) 109 | 110 | is equivalent to: 111 | 112 | set title "Sine vs Cosine" 113 | plot sin(x),cos(x) 114 | 115 | """ 116 | @spec plots(list(command())) :: list() 117 | def plots(commands) do 118 | [:plot, list(commands)] 119 | end 120 | 121 | @doc """ 122 | Build a comma separated list of two or more overlayed 3D plots (as 2D projections). 123 | 124 | Only useful inside `plot/1`: 125 | 126 | import Gnuplot 127 | 128 | plot([ 129 | [:set, :grid], 130 | splots([ 131 | ['x**2+y**2'], 132 | ['x**2-y**2'] 133 | ]) 134 | ]) 135 | 136 | """ 137 | @spec splots(list(command())) :: list() 138 | def splots(commands) do 139 | [:splot, list(commands)] 140 | end 141 | 142 | @doc "Build a comma separated list from a list of terms." 143 | def list(xs) when is_list(xs), do: %Commands.List{xs: xs} 144 | 145 | @doc "Build a comma separated list of two terms." 146 | def list(a, b), do: %Commands.List{xs: [a, b]} 147 | 148 | @doc "Build a comma separated list of three terms." 149 | def list(a, b, c), do: %Commands.List{xs: [a, b, c]} 150 | 151 | @doc "Build a comma separated list of four terms." 152 | def list(a, b, c, d), do: %Commands.List{xs: [a, b, c, d]} 153 | 154 | @doc "Build a comma separated list of five terms." 155 | def list(a, b, c, d, e), do: %Commands.List{xs: [a, b, c, d, e]} 156 | 157 | @doc "Build a comma separated list of six terms." 158 | def list(a, b, c, d, e, f), do: %Commands.List{xs: [a, b, c, d, e, f]} 159 | end 160 | -------------------------------------------------------------------------------- /lib/gnuplot/bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Gnuplot.Bin do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Find the gnuplot executable. 6 | """ 7 | @spec gnuplot_bin() :: {:error, :gnuplot_missing} | {:ok, :file.name()} 8 | def gnuplot_bin do 9 | case :os.find_executable(String.to_charlist("gnuplot")) do 10 | false -> {:error, :gnuplot_missing} 11 | path -> {:ok, path} 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gnuplot/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Gnuplot.Commands do 2 | @moduledoc """ 3 | Protocol that convert Elixir terms to Gnuplot commands. 4 | """ 5 | 6 | defprotocol Command do 7 | @spec formatg(term()) :: String.t() 8 | 9 | @doc "Format Elixir term as a Gnuplot String" 10 | def formatg(cmd) 11 | end 12 | 13 | defimpl Command, for: Atom do 14 | def formatg(a), do: Atom.to_string(a) 15 | end 16 | 17 | defimpl Command, for: Float do 18 | def formatg(x), do: Float.to_string(x) 19 | end 20 | 21 | defimpl Command, for: Integer do 22 | def formatg(x), do: Integer.to_string(x) 23 | end 24 | 25 | defimpl Command, for: BitString do 26 | def formatg(s) when is_binary(s), do: "\"" <> String.replace(s, "\"", "'") <> "\"" 27 | end 28 | 29 | defimpl Command, for: Range do 30 | def formatg(%{first: f, last: l}) do 31 | "[" <> Command.formatg(f) <> ":" <> Command.formatg(l) <> "]" 32 | end 33 | end 34 | 35 | defimpl Command, for: List do 36 | @spec formatg(maybe_improper_list()) :: binary() 37 | def formatg(xs = [x | _]) when is_atom(x) or is_binary(x) do 38 | Enum.map_join(xs, " ", &Command.formatg/1) 39 | end 40 | 41 | def formatg(xs), do: List.to_string(xs) 42 | end 43 | 44 | defmodule List do 45 | @moduledoc """ 46 | Comma separated list. 47 | 48 | Most lists are joined with whitespace, however the plot and splot commands require multiple plots to be comma separated. 49 | """ 50 | defstruct xs: [] 51 | end 52 | 53 | defimpl Command, for: List do 54 | def formatg(%{xs: xs}) do 55 | Enum.map_join(xs, ",", &Command.formatg/1) 56 | end 57 | end 58 | 59 | @doc """ 60 | Convert Elixir terms to Gnuplot strings. 61 | """ 62 | @spec format(list(list())) :: String.t() 63 | def format(cmds) do 64 | Enum.map_join(cmds, ";\n", &Command.formatg/1) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/gnuplot/dataset.ex: -------------------------------------------------------------------------------- 1 | defmodule Gnuplot.Dataset do 2 | @moduledoc false 3 | 4 | @type point :: list(number() | String.t()) | tuple() 5 | @type t :: list(point()) 6 | 7 | @gnuplot_end_row "\n" 8 | @gnuplot_end_data "\ne\n" 9 | 10 | @doc """ 11 | Convert Elixir lists to Gnuplot STDIN text. 12 | 13 | Datasets must be Enumerable and can be Streams. 14 | See the [target format](http://www.gnuplotting.org/tag/standard-input/) 15 | """ 16 | def format_datasets(datasets) do 17 | Stream.flat_map(datasets, &format_dataset/1) 18 | end 19 | 20 | defp format_dataset(dataset) do 21 | dataset 22 | |> Stream.map(&format_point/1) 23 | |> Stream.intersperse(@gnuplot_end_row) 24 | |> Stream.concat([@gnuplot_end_data]) 25 | end 26 | 27 | @spec format_point(point()) :: String.t() 28 | defp format_point(point) when is_tuple(point) do 29 | point |> Tuple.to_list() |> format_point() 30 | end 31 | 32 | defp format_point(point) do 33 | Enum.map_join(point, " ", &to_str/1) 34 | end 35 | 36 | defp to_str(f) when is_float(f), do: Float.to_string(f) 37 | defp to_str(i) when is_integer(i), do: Integer.to_string(i) 38 | 39 | defp to_str(s) when is_binary(s) do 40 | if contains_space?(s) do 41 | "\"" <> s <> "\"" 42 | else 43 | s 44 | end 45 | end 46 | 47 | def contains_space?(s), do: String.contains?(s, " ") 48 | end 49 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Gnuplot.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gnuplot, 7 | version: "1.22.270", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | description: "Interface between Elixir and Gnuplot graphing library", 11 | deps: deps(), 12 | dialyzer: [ 13 | flags: [], 14 | plt_add_apps: [:mix], 15 | remove_defaults: [:unknown] 16 | ], 17 | package: package(), 18 | source_url: "https://github.com/devstopfix/gnuplot-elixir" 19 | ] 20 | end 21 | 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | defp deps do 29 | [ 30 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 31 | {:dialyxir, "~> 1.2", only: [:dev], runtime: false}, 32 | {:ex_doc, "~> 0.28"} 33 | ] 34 | end 35 | 36 | defp package do 37 | [ 38 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 39 | maintainers: ["James Every"], 40 | licenses: ["EPL-2.0"], 41 | links: %{ 42 | "GitHub" => "https://github.com/devstopfix/gnuplot-elixir", 43 | "Gnuplot" => "http://www.gnuplot.info/", 44 | "Travis CI" => "https://travis-ci.org/devstopfix/gnuplot-elixir" 45 | } 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, 4 | "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, 5 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, 7 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 8 | "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, 9 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 10 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 11 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 13 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 14 | "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 16 | } 17 | -------------------------------------------------------------------------------- /test/gnuplot_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GnuplotTest do 2 | use ExUnit.Case 3 | alias Gnuplot, as: G 4 | alias Gnuplot.Commands, as: C 5 | alias Gnuplot.Dataset, as: D 6 | 7 | test "List of commands" do 8 | assert "set xtics off;\nset ytics off" == 9 | C.format([[:set, :xtics, :off], [:set, :ytics, :off]]) 10 | end 11 | 12 | test "Word sigil" do 13 | assert "set xtics off;\nset ytics off" == 14 | C.format([~w(set xtics off)a, ~w(set ytics off)a]) 15 | end 16 | 17 | test "String literals" do 18 | assert "plot cos(x)" == C.format([[:plot, 'cos(x)']]) 19 | end 20 | 21 | test "Strings and literals" do 22 | assert "plot sin(x) title \"Sine Wave\"" == C.format([[:plot, 'sin(x)', :title, "Sine Wave"]]) 23 | end 24 | 25 | test "Plot range" do 26 | assert "plot [0:5]" == C.format([[:plot, 0..5]]) 27 | end 28 | 29 | test "Title string" do 30 | assert "set title \"simple space\"" == C.format([[:set, :title, "simple space"]]) 31 | end 32 | 33 | test "Title apostrophe" do 34 | assert "set title \"simple's\"" == C.format([[:set, :title, "simple's"]]) 35 | end 36 | 37 | test "Datasets" do 38 | input = [[[1, 1], [1, 2]], [[2, 3], [2, 4], [2, 5]]] 39 | expected = ["1 1", "\n", "1 2", "\ne\n", "2 3", "\n", "2 4", "\n", "2 5", "\ne\n"] 40 | 41 | assert expected == 42 | input |> D.format_datasets() |> Enum.to_list() 43 | end 44 | 45 | test "Comma separated lists of plots using arity 2" do 46 | assert "plot a with lines,b with points" == 47 | C.format([[:plot, G.list([:a, :with, :lines], [:b, :with, :points])]]) 48 | end 49 | 50 | test "Comma separated lists of plots using arity 1 and sublists" do 51 | assert "plot a with lines,b with points" == 52 | C.format([[:plot, G.list([[:a, :with, :lines], [:b, :with, :points]])]]) 53 | end 54 | 55 | @tag gnuplot: true 56 | test "Gnuplot is installed" do 57 | assert {:ok, path} = Gnuplot.Bin.gnuplot_bin() 58 | assert File.exists?(path) 59 | end 60 | 61 | @tag gnuplot: true 62 | test "Simple plot with single dataset" do 63 | plot = [[:plot, "-", :with, :lines]] 64 | expected = "plot \"-\" with lines" 65 | assert {:ok, expected} == G.plot(plot, [[[0, 0], [1, 2], [2, 4]]]) 66 | end 67 | 68 | @tag gnuplot: true 69 | test "Scatter plot" do 70 | dataset = for _ <- 0..1000, do: [:rand.uniform(), :rand.normal()] 71 | plot = [[:set, :title, "rand uniform vs normal"], [:plot, "-", :with, :points]] 72 | assert {:ok, _} = G.plot(plot, [dataset]) 73 | end 74 | 75 | @tag gnuplot: true 76 | test "3d splot" do 77 | plot = [ 78 | [:set, :xrange, -3..3], 79 | [:set, :yrange, -3..3], 80 | G.splots([['x**2+y**2'], ['x**2-y**2']]) 81 | ] 82 | 83 | expected = "set xrange [-3:3];\nset yrange [-3:3];\nsplot x**2+y**2,x**2-y**2" 84 | assert {:ok, expected} == G.plot(plot) 85 | end 86 | 87 | @tag gnuplot: true 88 | test "Error stdout" do 89 | dataset = for _ <- 1..3, do: [:rand.uniform(), :rand.normal()] 90 | plot = [[:plot, "-", :with, :trapezoids]] 91 | assert {:error, _, errors} = G.plot(plot, [dataset]) 92 | assert Enum.join(errors) =~ "unrecognized plot type" 93 | end 94 | 95 | test "Strings with spaces in datasets" do 96 | input = [[0, "label", 100], [1, "label2", 450], [2, "bar label", 75]] 97 | expected = ["0 label 100", "\n", "1 label2 450", "\n", "2 \"bar label\" 75", "\ne\n"] 98 | 99 | assert expected == 100 | [input] |> D.format_datasets() |> Enum.to_list() 101 | end 102 | 103 | test "Multiplot with two plot(s)" do 104 | input = [[:set, :multiplot, :layout, '2,1'], [:plot, 'sin(x)'], [:plot, 'cos(x)']] 105 | expected = "set multiplot layout 2,1;\nplot sin(x);\nplot cos(x)" 106 | 107 | assert expected == C.format(input) 108 | end 109 | 110 | test "Multiplot with plots" do 111 | input = [[:set, :multiplot, :layout, '2,1'], G.plots([['sin(x)'], ['cos(x)']])] 112 | expected = "set multiplot layout 2,1;\nplot sin(x),cos(x)" 113 | 114 | assert expected == C.format(input) 115 | end 116 | 117 | test "Multiplot with list" do 118 | input = [[:set, :multiplot, :layout, '2,1'], [:plot, G.list([['sin(x)'], ['cos(x)']])]] 119 | expected = "set multiplot layout 2,1;\nplot sin(x),cos(x)" 120 | 121 | assert expected == C.format(input) 122 | end 123 | 124 | test "Splots" do 125 | input = G.splots([['x**2+y**2'], ['x**2-y**2']]) 126 | expected = "splot x**2+y**2,x**2-y**2" 127 | assert expected == C.format([input]) 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------