├── .gitignore ├── LICENSE.matplotlib.md ├── LICENSE.md ├── README.md ├── binding.gyp ├── examples ├── show.png └── subplot.png ├── package-lock.json ├── package.json ├── src ├── matplotlib.cc ├── matplotlib.h └── matplotlib.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .vscode 4 | -------------------------------------------------------------------------------- /LICENSE.matplotlib.md: -------------------------------------------------------------------------------- 1 | License agreement for matplotlib 1.5.1 2 | 1. This LICENSE AGREEMENT is between the Matplotlib Development Team (“MDT”), and the Individual or Organization (“Licensee”) accessing and otherwise using matplotlib software in source or binary form and its associated documentation. 3 | 4 | 2. Subject to the terms and conditions of this License Agreement, MDT hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use matplotlib 1.5.1 alone or in any derivative version, provided, however, that MDT’s License Agreement and MDT’s notice of copyright, i.e., “Copyright (c) 2012-2013 Matplotlib Development Team; All Rights Reserved” are retained in matplotlib 1.5.1 alone or in any derivative version prepared by Licensee. 5 | 6 | 3. In the event Licensee prepares a derivative work that is based on or incorporates matplotlib 1.5.1 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to matplotlib 1.5.1. 7 | 8 | 4. MDT is making matplotlib 1.5.1 available to Licensee on an “AS IS” basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 1.5.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 9 | 10 | 5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB 1.5.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING MATPLOTLIB 1.5.1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 11 | 12 | 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 13 | 14 | 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between MDT and Licensee. This License Agreement does not grant permission to use MDT trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 15 | 16 | 8. By copying, installing or otherwise using matplotlib 1.5.1, Licensee agrees to be bound by the terms and conditions of this License Agreement. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | #The MIT License (MIT) 2 | 3 | *Copyright (c) 2016 Mateo Gianolio* 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matplotnode 2 | 3 | C++ bindings for Node.js exposing a subset of [matplotlib](http://matplotlib.org/)'s functionality through the [CPython API](https://docs.python.org/2/extending/embedding.html). Inspired by [matplotlib-cpp](https://github.com/lava/matplotlib-cpp) by [lava](https://github.com/lava). Useful for scientific plotting. 4 | 5 | ### Requirements 6 | 7 | * Python 2.7 (might work on Python 3, not tested yet) 8 | * [matplotlib](http://matplotlib.org/) 9 | 10 | ### Usage 11 | 12 | ```bash 13 | $ npm install matplotnode 14 | ``` 15 | 16 | ```javascript 17 | const plt = require('matplotnode'); 18 | ``` 19 | 20 | ### Bindings 21 | 22 | - [x] `plot([x], y, ...kwargs)`* 23 | - [x] `subplot(str)` 24 | - [x] `show()` 25 | - [x] `legend()` 26 | - [x] `grid(bool active)` 27 | - [x] `save(filename)` 28 | - [x] `xlim(from, to)` 29 | - [x] `ylim(from, to)` 30 | - [x] `title(name)` 31 | - [x] `axis(axis)` 32 | - [x] `xlabel(name)` 33 | - [x] `ylabel(name)` 34 | - [x] `clf()` 35 | - [x] `cla()` 36 | - [x] `close()` 37 | - [x] `xkcd()` 38 | 39 | **See how `kwargs` are implemented in `test.js`* 40 | 41 | ### Example 42 | 43 | ```javascript 44 | const plt = require('matplotnode'); 45 | const x = new Array(100).fill(0).map((x, i) => i / Math.PI); 46 | 47 | // xkcd-style plot :) 48 | plt.xkcd(); 49 | 50 | plt.subplot("211"); 51 | plt.title('trig'); 52 | plt.plot(x, x.map(Math.sin), 'color=r', 'label=sin(x)'); 53 | plt.plot(x, x.map(Math.cos), 'color=g', 'label=cos(x)'); 54 | plt.legend(); 55 | 56 | plt.subplot("212"); 57 | plt.plot(x, x.map(Math.sin).map((t, i) => t * i), 'color=b', 'label=x * sin(x)', 'marker=o', 'linestyle=None'); 58 | plt.legend(); 59 | plt.ylim(-100, 100); 60 | 61 | plt.save("./examples/subplot.png"); 62 | ``` 63 | 64 | ![subplot example](examples/subplot.png) 65 | 66 | 67 | ```javascript 68 | const plt = require('matplotnode'); 69 | const x = new Array(100).fill(0).map((x, i) => i / Math.PI); 70 | 71 | plt.plot(x, x.map(Math.sin)); 72 | plt.grid(true); 73 | plt.show(); 74 | ``` 75 | 76 | ![show example](examples/show.png) 77 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "matplotlib", 5 | "sources": [ "src/matplotlib.cc" ], 6 | "libraries": [ 7 | "-ldl" 8 | ], 9 | "conditions": [ 10 | ['OS=="mac"', { 11 | "xcode_settings": { 12 | "OTHER_CFLAGS": [ 13 | "& info) { 5 | v8::Isolate* isolate = info.GetIsolate(); 6 | 7 | PyObject *args, *kwargs, *data, *result; 8 | uint32_t arglen = info.Length(); 9 | uint32_t datalen; 10 | 11 | args = PyTuple_New(2); 12 | kwargs = PyDict_New(); 13 | 14 | for (uint32_t i = 0; i < arglen; i++) { 15 | if (info[i]->IsString()) { 16 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[i].As())); 17 | 18 | unsigned long eq = s.find("="); 19 | if (eq != std::string::npos) { 20 | std::string left = s.substr(0, eq); 21 | std::string right = s.substr(eq + 1, s.size() - 1); 22 | 23 | PyDict_SetItemString(kwargs, left.c_str(), PyString_FromString(right.c_str())); 24 | } 25 | } else if (info[i]->IsArray()) { 26 | v8::Local array = info[i].As(); 27 | datalen = array->Length(); 28 | data = PyList_New(datalen); 29 | 30 | for (uint32_t j = 0; j < datalen; j++) 31 | PyList_SetItem(data, j, PyFloat_FromDouble(array->Get(isolate->GetCurrentContext(), j).ToLocalChecked().As()->Value())); 32 | PyTuple_SetItem(args, i, data); 33 | } 34 | } 35 | 36 | result = PyObject_Call(interpreter::get().plot, args, kwargs); 37 | 38 | Py_DECREF(args); 39 | Py_DECREF(kwargs); 40 | Py_XDECREF(result); 41 | } 42 | 43 | void subplot(const v8::FunctionCallbackInfo& info) { 44 | v8::Isolate* isolate = info.GetIsolate(); 45 | 46 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 47 | PyObject *args = PyTuple_New(1); 48 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 49 | 50 | PyObject *result = PyObject_CallObject(interpreter::get().subplot, args); 51 | 52 | Py_DECREF(args); 53 | Py_XDECREF(result); 54 | } 55 | 56 | void show(const v8::FunctionCallbackInfo& info) { 57 | PyObject *result = PyObject_CallObject(interpreter::get().show, interpreter::get().empty_tuple); 58 | Py_XDECREF(result); 59 | } 60 | 61 | void legend(const v8::FunctionCallbackInfo& info) { 62 | PyObject *result = PyObject_CallObject(interpreter::get().legend, interpreter::get().empty_tuple); 63 | Py_XDECREF(result); 64 | } 65 | 66 | void grid(const v8::FunctionCallbackInfo& info) { 67 | PyObject *flag = info[0].As()->Value() ? Py_True : Py_False; 68 | PyObject *args = PyTuple_New(1); 69 | PyTuple_SetItem(args, 0, flag); 70 | 71 | PyObject *result = PyObject_CallObject(interpreter::get().grid, args); 72 | 73 | Py_DECREF(args); 74 | Py_XDECREF(result); 75 | } 76 | 77 | void save(const v8::FunctionCallbackInfo& info) { 78 | v8::Isolate* isolate = info.GetIsolate(); 79 | 80 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 81 | PyObject *args = PyTuple_New(1); 82 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 83 | 84 | PyObject *result = PyObject_CallObject(interpreter::get().save, args); 85 | 86 | Py_DECREF(args); 87 | Py_XDECREF(result); 88 | } 89 | 90 | void xlim(const v8::FunctionCallbackInfo& info) { 91 | PyObject *list = PyList_New(2); 92 | PyObject *args = PyTuple_New(1); 93 | 94 | PyList_SetItem(list, 0, PyFloat_FromDouble(info[0].As()->Value())); 95 | PyList_SetItem(list, 1, PyFloat_FromDouble(info[1].As()->Value())); 96 | PyTuple_SetItem(args, 0, list); 97 | 98 | PyObject *result = PyObject_CallObject(interpreter::get().xlim, args); 99 | 100 | Py_DECREF(args); 101 | Py_XDECREF(result); 102 | } 103 | 104 | void ylim(const v8::FunctionCallbackInfo& info) { 105 | PyObject *list = PyList_New(2); 106 | PyObject *args = PyTuple_New(1); 107 | 108 | PyList_SetItem(list, 0, PyFloat_FromDouble(info[0].As()->Value())); 109 | PyList_SetItem(list, 1, PyFloat_FromDouble(info[1].As()->Value())); 110 | PyTuple_SetItem(args, 0, list); 111 | 112 | PyObject *result = PyObject_CallObject(interpreter::get().ylim, args); 113 | 114 | Py_DECREF(args); 115 | Py_XDECREF(result); 116 | } 117 | 118 | void title(const v8::FunctionCallbackInfo& info) { 119 | v8::Isolate* isolate = info.GetIsolate(); 120 | 121 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 122 | PyObject *args = PyTuple_New(1); 123 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 124 | 125 | PyObject *result = PyObject_CallObject(interpreter::get().title, args); 126 | 127 | Py_DECREF(args); 128 | Py_XDECREF(result); 129 | } 130 | 131 | void axis(const v8::FunctionCallbackInfo& info) { 132 | v8::Isolate* isolate = info.GetIsolate(); 133 | 134 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 135 | PyObject *args = PyTuple_New(1); 136 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 137 | 138 | PyObject *result = PyObject_CallObject(interpreter::get().axis, args); 139 | 140 | Py_DECREF(args); 141 | Py_XDECREF(result); 142 | } 143 | 144 | void xlabel(const v8::FunctionCallbackInfo& info) { 145 | v8::Isolate* isolate = info.GetIsolate(); 146 | 147 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 148 | PyObject *args = PyTuple_New(1); 149 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 150 | 151 | PyObject *result = PyObject_CallObject(interpreter::get().xlabel, args); 152 | 153 | Py_DECREF(args); 154 | Py_XDECREF(result); 155 | } 156 | 157 | void ylabel(const v8::FunctionCallbackInfo& info) { 158 | v8::Isolate* isolate = info.GetIsolate(); 159 | 160 | std::string s = std::string(*v8::String::Utf8Value(isolate, info[0].As())); 161 | PyObject *args = PyTuple_New(1); 162 | PyTuple_SetItem(args, 0, PyString_FromString(s.c_str())); 163 | 164 | PyObject *result = PyObject_CallObject(interpreter::get().ylabel, args); 165 | 166 | Py_DECREF(args); 167 | Py_XDECREF(result); 168 | } 169 | 170 | void clf(const v8::FunctionCallbackInfo& info) { 171 | PyObject *result = PyObject_CallObject(interpreter::get().clf, interpreter::get().empty_tuple); 172 | Py_XDECREF(result); 173 | } 174 | 175 | void cla(const v8::FunctionCallbackInfo& info) { 176 | PyObject *result = PyObject_CallObject(interpreter::get().cla, interpreter::get().empty_tuple); 177 | Py_XDECREF(result); 178 | } 179 | 180 | void close(const v8::FunctionCallbackInfo& info) { 181 | PyObject *result = PyObject_CallObject(interpreter::get().close, interpreter::get().empty_tuple); 182 | Py_XDECREF(result); 183 | } 184 | 185 | void xkcd(const v8::FunctionCallbackInfo& info) { 186 | PyObject *kwargs = PyDict_New(); 187 | PyObject *result = PyObject_Call(interpreter::get().xkcd, interpreter::get().empty_tuple, kwargs); 188 | 189 | Py_DECREF(kwargs); 190 | Py_DECREF(result); 191 | } 192 | } 193 | 194 | void init(v8::Local exports) { 195 | NODE_SET_METHOD(exports, "plot", plt::plot); 196 | NODE_SET_METHOD(exports, "subplot", plt::subplot); 197 | NODE_SET_METHOD(exports, "show", plt::show); 198 | NODE_SET_METHOD(exports, "legend", plt::legend); 199 | NODE_SET_METHOD(exports, "grid", plt::grid); 200 | NODE_SET_METHOD(exports, "save", plt::save); 201 | NODE_SET_METHOD(exports, "xlim", plt::xlim); 202 | NODE_SET_METHOD(exports, "ylim", plt::ylim); 203 | NODE_SET_METHOD(exports, "title", plt::title); 204 | NODE_SET_METHOD(exports, "axis", plt::axis); 205 | NODE_SET_METHOD(exports, "xlabel", plt::xlabel); 206 | NODE_SET_METHOD(exports, "ylabel", plt::ylabel); 207 | NODE_SET_METHOD(exports, "clf", plt::clf); 208 | NODE_SET_METHOD(exports, "cla", plt::cla); 209 | NODE_SET_METHOD(exports, "close", plt::close); 210 | NODE_SET_METHOD(exports, "xkcd", plt::xkcd); 211 | } 212 | 213 | NODE_MODULE(matplotlib, init) -------------------------------------------------------------------------------- /src/matplotlib.h: -------------------------------------------------------------------------------- 1 | #ifndef MATPLOTLIB_H 2 | #define MATPLOTLIB_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef linux 9 | #include 10 | #endif 11 | 12 | #if PY_MAJOR_VERSION >= 3 13 | #define PyString_FromString PyUnicode_FromString 14 | #endif 15 | 16 | namespace plt { 17 | struct interpreter { 18 | public: 19 | PyObject *plot; 20 | PyObject *subplot; 21 | PyObject *show; 22 | PyObject *legend; 23 | PyObject *grid; 24 | PyObject *save; 25 | PyObject *xlim; 26 | PyObject *ylim; 27 | PyObject *title; 28 | PyObject *axis; 29 | PyObject *xlabel; 30 | PyObject *ylabel; 31 | PyObject *clf; 32 | PyObject *cla; 33 | PyObject *close; 34 | PyObject *xkcd; 35 | 36 | PyObject *empty_tuple; 37 | 38 | static interpreter& get() { 39 | static interpreter context; 40 | return context; 41 | } 42 | private: 43 | interpreter() { 44 | #if PY_MAJOR_VERSION >= 3 45 | wchar_t name[] = L"matplotnode"; 46 | #else 47 | char name[] = "matplotnode"; 48 | #endif 49 | Py_SetProgramName(name); 50 | 51 | #ifdef linux 52 | #if PY_MAJOR_VERSION >= 3 53 | dlopen("libpython3.so", RTLD_LAZY | RTLD_GLOBAL); 54 | #else 55 | dlopen("libpython2.7.so", RTLD_LAZY | RTLD_GLOBAL); 56 | #endif 57 | #endif 58 | Py_Initialize(); 59 | 60 | PyObject *matplotlibname = PyString_FromString("matplotlib"); 61 | PyObject* matplotlib = PyImport_Import(matplotlibname); 62 | Py_DECREF(matplotlibname); 63 | if (!matplotlib) { 64 | PyErr_Print(); 65 | fprintf(stderr, "Could not import matplotlib.pyplot.\n"); 66 | return; 67 | } 68 | 69 | PyObject_CallMethod(matplotlib, const_cast("use"), const_cast("s"), "TkAgg"); 70 | 71 | PyObject *pyplotname = PyString_FromString("matplotlib.pyplot"); 72 | PyObject *pyplot = PyImport_Import(pyplotname); 73 | Py_DECREF(pyplotname); 74 | 75 | if (!pyplot) { 76 | PyErr_Print(); 77 | fprintf(stderr, "Could not import matplotlib.pyplot.\n"); 78 | return; 79 | } 80 | 81 | plot = PyObject_GetAttrString(pyplot, "plot"); 82 | subplot = PyObject_GetAttrString(pyplot, "subplot"); 83 | show = PyObject_GetAttrString(pyplot, "show"); 84 | legend = PyObject_GetAttrString(pyplot, "legend"); 85 | grid = PyObject_GetAttrString(pyplot, "grid"); 86 | save = PyObject_GetAttrString(pyplot, "savefig"); 87 | xlim = PyObject_GetAttrString(pyplot, "xlim"); 88 | ylim = PyObject_GetAttrString(pyplot, "ylim"); 89 | title = PyObject_GetAttrString(pyplot, "title"); 90 | axis = PyObject_GetAttrString(pyplot, "axis"); 91 | xlabel = PyObject_GetAttrString(pyplot, "xlabel"); 92 | ylabel = PyObject_GetAttrString(pyplot, "ylabel"); 93 | clf = PyObject_GetAttrString(pyplot, "clf"); 94 | cla = PyObject_GetAttrString(pyplot, "cla"); 95 | close = PyObject_GetAttrString(pyplot, "close"); 96 | xkcd = PyObject_GetAttrString(pyplot, "xkcd"); 97 | 98 | if (!plot 99 | || !subplot 100 | || !show 101 | || !legend 102 | || !grid 103 | || !save 104 | || !xlim 105 | || !ylim 106 | || !title 107 | || !axis 108 | || !xlabel 109 | || !ylabel 110 | || !clf 111 | || !cla 112 | || !close 113 | || !xkcd) { 114 | PyErr_Print(); 115 | fprintf(stderr, "Error loading matplotlib functions.\n"); 116 | return; 117 | } 118 | 119 | if (!PyCallable_Check(plot) 120 | || !PyCallable_Check(subplot) 121 | || !PyCallable_Check(show) 122 | || !PyCallable_Check(legend) 123 | || !PyCallable_Check(grid) 124 | || !PyCallable_Check(save) 125 | || !PyCallable_Check(xlim) 126 | || !PyCallable_Check(ylim) 127 | || !PyCallable_Check(title) 128 | || !PyCallable_Check(axis) 129 | || !PyCallable_Check(xlabel) 130 | || !PyCallable_Check(ylabel) 131 | || !PyCallable_Check(clf) 132 | || !PyCallable_Check(cla) 133 | || !PyCallable_Check(close) 134 | || !PyCallable_Check(xkcd)) { 135 | PyErr_Print(); 136 | fprintf(stderr, "One or more of the matplotlib functions are not callable.\n"); 137 | return; 138 | } 139 | 140 | empty_tuple = PyTuple_New(0); 141 | } 142 | 143 | ~interpreter() { 144 | Py_Finalize(); 145 | } 146 | }; 147 | 148 | void plot(v8::FunctionCallbackInfo& info); 149 | void subplot(v8::FunctionCallbackInfo& info); 150 | void show(v8::FunctionCallbackInfo& info); 151 | void legend(v8::FunctionCallbackInfo& info); 152 | void grid(v8::FunctionCallbackInfo& info); 153 | void save(v8::FunctionCallbackInfo& info); 154 | void xlim(v8::FunctionCallbackInfo& info); 155 | void ylim(v8::FunctionCallbackInfo& info); 156 | void title(v8::FunctionCallbackInfo& info); 157 | void axis(v8::FunctionCallbackInfo& info); 158 | void xlabel(v8::FunctionCallbackInfo& info); 159 | void ylabel(v8::FunctionCallbackInfo& info); 160 | void clf(v8::FunctionCallbackInfo& info); 161 | void cla(v8::FunctionCallbackInfo& info); 162 | void close(v8::FunctionCallbackInfo& info); 163 | void xkcd(v8::FunctionCallbackInfo& info); 164 | } 165 | 166 | #endif -------------------------------------------------------------------------------- /src/matplotlib.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | module.exports = require('../build/Release/matplotlib'); 4 | }()); -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const plt = require('./build/Release/matplotlib'); 5 | const x = new Array(100).fill(0).map((x, i) => i / Math.PI); 6 | 7 | plt.xkcd(); 8 | 9 | plt.subplot("211"); 10 | plt.title('trig'); 11 | plt.plot(x, x.map(Math.sin), 'color=r', 'label=sin(x)'); 12 | plt.plot(x, x.map(Math.cos), 'color=g', 'label=cos(x)'); 13 | plt.legend(); 14 | 15 | plt.subplot("212"); 16 | plt.plot(x, x.map(Math.sin).map((t, i) => t * i), 'color=b', 'label=x * sin(x)', 'marker=o', 'linestyle=None'); 17 | plt.legend(); 18 | plt.ylim(-100, 100); 19 | 20 | plt.save("./examples/subplot.png"); 21 | }()); --------------------------------------------------------------------------------