Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plotting framework #673

Closed
wants to merge 26 commits into from
Closed

Plotting framework #673

wants to merge 26 commits into from

Conversation

Krastanov
Copy link
Member

Most of the new lines are from the ipython notebooks in ./examples.

This proposal for a plotting framework easily supports multiple backends (the wish that Certik expressed on multiple occasions). For the moment only matplotlib and textplot have backends (simple ones).

See the docstrings for the module and for the Plot class for details. Example usage is present in example files in the root folder.

The base idea is to have simple representation for the data to be plotted (DataSeries), simple backends implementing some or all of the api and the class Plot that is a nice and simple interface for all this. For anything fancy the user should access directly the backend. For example the matplotlib backend will return the figure/axe/lines instances and the user can work on them.

TODO: current issues (many thanks to asmeurer and miham for raising them):

DONE:

NEW TODO

#1

Plot() should not do all this magic. All the magic should be in plot()
and Series().

  • Plot() should accept only instances of subclasses of BaseSeries in
    its constructor.
  • plot() should be fixed to work after the change to Plot()
  • the documentation should explicitly mention that if any heuristics
    is to be added it should be contained in Series() and plot()
  • the name of Series() should change to something like HeuristicSeries()
    Soc final fixed #2

Add explicit plot functions:
plot_line, plot_surface, plot_parametric_surface, etc
These again should require fully explicit syntax, no guessing like in plot()
#3

The api should be as follows (both for plot() and for the explicit
plot_something()):

  • plot_some_type_of_graph(expression, tuple_of_variable_and_range, ...)
    ex: plot_surface(x+y, (x, 1, 2), (y, 1, 2))
    result: obvious
  • plot_some_type_of_graph(list_of_expressions, tuple_of_variable_and_range, ...)
    ex: plot_surface([x+y, x-y], (x, 1, 2), (y, 1, 2))
    result: plots two surfaces
  • plot_some_type_of_graph(many_tuples_of(expression,
    tuple_of_variable_and_range, ...))
    ex: plot_surface((x+y, (x, 1, 2), (y, 1, 2)), (x-y, (x, 1, 2), (y, 1, 2)))
    result: the same as the previous example
    Improve collect so that you can use Wild in the pattern #4

The ipython profile should be updated

@asmeurer
Copy link
Member

At least on my computer, the matplotlib plots don't stay open with the test script, so I can't really see them.

@Krastanov
Copy link
Member Author

@asmeurer, try it in interactive mode (-i). The test.py is meant more for copy-pasting.

Just to be clear (I'm adding it to the docstrings right now) the work flow I imagine is to use this module for making a not-too-advanced plot and then using directly the backend (mainly matplotlib) to do any fancy fine-tuning.

The proposition here may be way too simplistic or way to complicated when the stated goals are taken in account. In any case I would like to hear what you think.

@Krastanov
Copy link
Member Author

Please also look at the docstring of newplot.Plot - it explains all the options and aesthetics (the choice of names is based loosely on The Grammar of Graphics and the ggplot from R).

@goodok
Copy link
Contributor

goodok commented Nov 14, 2011

Please, replace Out [1]: to >>> how it described in [1].
Thereafter thos doctests in the docstrings can be tested automatically. So I will be sure that they are written correctly and in the future development workflow they will not failed.

[1] https://github.com/sympy/sympy/wiki/Running-tests

@Krastanov
Copy link
Member Author

There is a reason those examples are not doctests - they were done in Ipython with Ipython's printing hooks, so they will not be the same as what comes from Cpython. As you can see I have not imported the needed objects either.

I suppose I'll rewrite those when I start writing tests. (At least I should.)


Examples:
In [10]: str2tree(str(Integral(x, (x, 1, y))))
Out[10]: ('', ('Integral(', 'x', '(x, 1, y)'), ')')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My console give me:
('', ('Integral(', 'x, (x, 1, y)'), ')')

The other examples are correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fixed, you are right

On 14 November 2011 19:24, Alexey U. Gudchenko <
reply@reply.github.com

wrote:

  • return (numpy_dict, )

+##############################################################################
+# The translator functions, tree parsers, etc.

+##############################################################################
+
+def str2tree(exprstr):

  • """Converts an expression string to a tree.
  • Functions are represented by ('func_name(', tree_of_arguments).
  • Other expressions are (head_string, mid_tree, tail_str).
  • Expressions that do not contain functions are directly returned.
  • Examples:
  • In [10]: str2tree(str(Integral(x, (x, 1, y))))
  • Out[10]: ('', ('Integral(', 'x', '(x, 1, y)'), ')')

My console give me:
('', ('Integral(', 'x, (x, 1, y)'), ')')

The other examples are correct.


Reply to this email directly or view it on GitHub:
https://github.com/sympy/sympy/pull/673/files#r227792

@asmeurer
Copy link
Member

Yes, I would minimize those doctests that use IPython to only those that specifically are showing what happens in IPython. Because those doctests will never be tested.

On the other hand, doctests that generate plots will have to be skipped anyway, but even so, we should assume that the average user does not use IPython.

@Krastanov
Copy link
Member Author

@asmeurer, On the mailing list @certik proposed to merge this sooner so it can be used, tested and worked on by other people. I agree with him.

The documentation is already written (in docstrings, not on sphinx). The plot function you proposed is there BUT for the moment it just passes its argument to Plot. The old Plot was moved, the documentation for the old plot on sphinx says that there is an alternative and the location of the doctests in the doctests utility was updated.

from sympy import * will import textplot, plot and the old Plot with a warning added. The warning says how to import the OLD Plot without warnings and how to import the new Plot.

The polar, cylindrical and spherical stuff won't be written very soon. The experimental_lambdify should be reviewed in depth.

@Krastanov
Copy link
Member Author

One important thing: the change of backends is not explained and it's difficult at the moment (the backend must be imported). But it defaults to matplotlib so there should be no problem.

On the other hand if there is no matplotlib on the system "from sympy import all" will raise an error.

@Krastanov
Copy link
Member Author

The last commit should take care of the problems arising from not having matplotlib installed.

@asmeurer
Copy link
Member

@asmeurer, On the mailing list @certik proposed to merge this sooner so it can be used, tested and worked on by other people. I agree with him.

I agree. The only thing that should be fixed before merging is a stable API, since that is annoying to change afterwords.

On the other hand if there is no matplotlib on the system "from sympy import all" will raise an error.

This is a problem SymPy should not require any dependencies by default.

By the way, this branch cannot be cleanly merged over master.

@Krastanov
Copy link
Member Author

The code is mostly ready. Please check the init.py file. It seems that the plot function is not always imported.

Waiting for a review.

@asmeurer
Copy link
Member

Please check the init.py file. It seems that the plot function is not always imported.

This should be fixed so that it's always imported, but raises ImportError if you try to use it without matplotlib (or, it moves on to the next library). Take a look at import_module() in sympy/external/importtools.py. The importing of any external libraries (numpy, matplotlib, etc.) should use this.

@Krastanov
Copy link
Member Author

The importing of module is now done. But there is another bug - plot(cos(sqrt(x2+y2))) is not working, it's a problem with the experimental_lambdify function that I'm using. I'll fix it later.

@mrocklin
Copy link
Member

What is the status on this?

@Krastanov
Copy link
Member Author

Works if you have installed the dependencies (matplotlib). I'm currently using it. Has mostly stable api and will be finished after my exams (or GCI). You can start reviewing but there are some obvious things that will change (adding tests, making it not crash on import if matplotlib is not installed, and so on).

The documentation (comments and docstrings) must be reviewed!
The experimental_lambdify function must be reviewed (it's private).

@Krastanov Krastanov closed this Jan 21, 2012
@Krastanov Krastanov reopened this Jan 21, 2012
@Krastanov
Copy link
Member Author

I have simplified some parts of the code (the oldest parts that have not changed as the code evolved).

And I have added tests. In the test folder I have placed reference png files that are compared to newly generated files each time the test runs. I am not sure whether this will work on different versions of matplotlib or other OSes.

@Krastanov
Copy link
Member Author

SymPy Bot Summary: There were test failures.

@Krastanov: Please fix the test failures.

Test results html report: http://reviews.sympy.org/report/agZzeW1weTNyDAsSBFRhc2sYtYEKDA

Interpreter: /usr/bin/python (2.7.1-final-0)
Architecture: Linux (64-bit)
Cache: yes
Test command: setup.py test
master hash: 9258bb9
branch hash: 666e34e2caa89385fbd970db44b8b0da669fbff2

Automatic review by SymPy Bot.

@Krastanov
Copy link
Member Author

@smichr and @asmeurer , would you be able to test this on windows and mac. I have tested it on two different ubuntu installations and it works ok (the failures are in test of integration and another concerning issue 2863, not in my code).

Please run only the new plotting tests: ./bin/test sympy/plotting/tests

You will absolutely need matplotlib from git (there are bugs in the latest stable release): https://github.com/matplotlib/matplotlib (I'm not sure how difficult it is to compile for windows/mac, if it is too much work I will just disable the tests that do not work in the stable release)

And you must install PIL (python-imaging-library). (needed only for the tests, not for plotting)

The tests will also print out a sequence of True/False values. Could you post them if there are any False?

if parent.legend:
self.ax.legend()
self.ax.legend_.set_visible(parent.legend)
elif hasattr(self.ax, 'ledend_'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I do not think so. legend_ is created after calling legend() and consequent manipulations are done on it. But maybe there is a better and more OOP method to write this? I learned most of matplotlib while writing this module so there may be some anti-patterns present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should be more specific :). In line 907, is it suppose to be legend_ or ledend_?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh god! You are right. Thanks for spotting this. I will add also a test for deleting a legend.

@miham
Copy link
Contributor

miham commented Jan 28, 2012

I've only used the new plot function for a couple plots, but so far I have to say it feels really nice. The notebook examples are also a joy to play with.

Some functionality i would like to see (but there's no hurry, I wouldn't mind implementing it myself when i find the time):

  • Change default location of the legend when matplotlib is used to 'best' (plt.plot(..., loc='best')). That way the legend box tries not to cover the curves.
  • Automatically draw new lines with different colors. In the following example all lines are blue,
p = plot(besselj(0, x), (x, 0, 10), show=False)
for i in xrange(1,5):
    p.extend(plot(besselj(i, x), (x, 0, 10), show=False))
p.legend = True
p.show()
  • Implement __iadd__ for Plot objects, so += can be used instead of extend function.

@Krastanov
Copy link
Member Author

Feel free to add those to the issue tracker. That way they will not be
lost. And I agree about each of those.

It would be great help if you can run the tests on this (you will need
matplotlib from git). For the moment they run well on my machine but
it is hard to test graphics so I imagine that there will be quite some
changes before the testing routines are ready. Those that are in the
code at the moment are just a preliminary idea that must be tested.

On 28 January 2012 23:11, Miha Marolt
reply@reply.github.com
wrote:

I've only used the new plot function for a couple plots, but so far I have to say it feels really nice. The notebook examples are also a joy to play with.

Some functionality i would like to see (but there's no hurry, I wouldn't mind implementing it myself when i find the time):

  • Change default location of the legend when matplotlib is used to 'best' (plt.plot(..., loc='best')). That way the legend box tries not to cover the curves.
  • Automatically draw new lines with different colors. In the following example all lines are blue,
p = plot(besselj(0, x), (x, 0, 10), show=False)
for i in xrange(1,5):
   p.extend(plot(besselj(i, x), (x, 0, 10), show=False))
p.legend = True
p.show()
  • Implement __iadd__ for Plot objects, so += can be used instead of extend function.

Reply to this email directly or view it on GitHub:
#673 (comment)

@miham
Copy link
Contributor

miham commented Jan 28, 2012

Feel free to add those to the issue tracker. That way they will not be lost. And I agree about each of those.

Should I add it to 2845 or create a new issue like "plotting enhancements".

It would be great help if you can run the tests on this (you will need matplotlib from git). For the moment they run well > on my machine but it is hard to test graphics so I imagine that there will be quite some changes before the testing
routines are ready. Those that are in the code at the moment are just a preliminary idea that must be tested.

Sure, I'll do it tomorrow.

@Krastanov
Copy link
Member Author

I would prefer a new issue (blocked on the current one). That way it
would be easier to close the current and focus on the next.

On 28 January 2012 23:26, Miha Marolt
reply@reply.github.com
wrote:

Feel free to add those to the issue tracker. That way they will not be lost. And I agree about each of those.

Should I add it to 2845 or create a new issue like "plotting enhancements".

It would be great help if you can run the tests on this (you will need matplotlib from git). For the moment they run well > on my machine but it is hard to test graphics so I imagine that there will be quite some changes before the testing
routines are ready. Those that are in the code at the moment are just a preliminary idea that must be tested.

Sure, I'll do it tomorrow.


Reply to this email directly or view it on GitHub:
#673 (comment)

and len(args[4]) == 3):
inst = ParametricSurfaceSeries(*args)
else:
raise ValueError('The supplied argument do not correspond to a'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argument => arguments

@asmeurer
Copy link
Member

So I've started playing around with this, now that I need to do some plotting for my homework. I get the following:

In [4]: a = plot(sqrt(-alpha - 1))
<string>:1: RuntimeWarning: invalid value encountered in sqrt

This does create the plot.

It also never returns, unless I close the plot. Is this the intended behavior? It isn't very helpful.

@asmeurer
Copy link
Member

Next question: what's the easiest way to plot multiple equations at once? I tried passing a list, but it just gave me an error. I tried .extend, but this didn't work (possibly because I had to close the other plot to get control back).

@asmeurer
Copy link
Member

OK, I figured it out. You have to pass them as 1-element tuples. I guess this makes sense because you can combine different kinds of plots, though it still seems to me that passing a list of multiple equations should work. I'll have to think about it.

If the version is less than 1.0.0 matplotlib is not imported and
ascii is used. If the version is less than 1.2.0 (not released yet)
the 3D surface coloring does not work.
A number of regressions were fixed.

There are now tests and doctest, that check for correct instantialization and
saving to disk of figures. It checks only the `plot()` function.

The `Plot()` class was simplified, as it was trying to be too many things at the
same time.

The `Series()` class was simplified for the same reason and it is now named
`OverloadedSeriesFactory()`.

Most of the docstrings were updated, however there is still much to be wanted
from the documentation of the module.
The tests were failing if numpy was not installed. A try except block was added
to check for import errors.
The SympyDeprecationWarning was moved from its original location. The change
was done in the master branch. The same change must be mirrored in this
development branch.
The following tests concern the detection of complex numbers in the plotting
routines. The coverage of experimental_lambdify is better, however still not
100% due to bugs in the vectorization routine (it does not detect examples like
sin(LambertW(x)) where a complex sympy expression is given to a python math
function).
@Krastanov
Copy link
Member Author

This is discussion from a pull request against this branch

Krastanov#5

About the review, everything looks fine. I think it is ready to go in. The only problem is show() is blocking, which will prevent updating of plot on the go. Is there any problem with using ion() ?

This should be discussed on the other pull request page, so the others can comment. Basically using ion() is the responsability of ipython and isympy, not the plotting module. It was already explained a few times how to do it in ipython.

@Krastanov
Copy link
Member Author

Here is the ipython_config that I am using.

https://gist.github.com/2775867

@catchmrbharath
Copy link
Contributor

I forgot about the mention of ion() previously and all the discussion about it here. I think this is ready to go in. I will be able to add a basic adaptive sampling for 2D after this gets merged.

@catchmrbharath
Copy link
Contributor

Will it be better to plot the real part of a complex value, or not plot the complex value? This has to be discussed before we add this.

@Krastanov
Copy link
Member Author

@asmeurer, This has gone through numerous revisions and now has a nice amount of tests (the coverage is). I am sure that there are still issues and that even small changes to the api are possible, however these will be difficult to happen without more widespread use. Thus, as @catchmrbharath has review it, I would like to get this merged.

An important outstanding issue is the fact that show() blocks the eventloop. However I think that this is an issue to be solved in isympy and ipython. This is also the stance that the matplotlib team has according to their webpage.

Adaptive sampling is already in a pull request against this branch. There is also a pull request changing the way that complex values are plotted.

@Krastanov
Copy link
Member Author

oops, this should have been "the coverage is ~90%".

# wrapping in complex((...).evalf()) and returning the real
# part.
if self.failure:
raise e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any particular reason why we are raising the exception. Repeated calls to to a vectorized_lambdify function will raise the exception when adaptive sampling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, the problem was somewhere else. Got why you raise the exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the question. There is infinite recursion without
this. Adaptive sampling has nothing to do with it. Finally, if you
have an expression that raises this error there is a bug.

On 24 May 2012 14:54, Bharath M R
reply@reply.github.com
wrote:

  •                  ('Symbolic value, can't compute' in str(e) or 'math domain error' in str(e))):
  •                # Almost all functions were translated to python math, but some
  •                # were left as sympy functions. They produced complex numbers.
  •                #   float(a+I*b) raises "Symbolic value, can't compute"
  •                #   math.sqrt(-1) raises "math domain error"
  •                # Solution: use cmath and vectorize the final lambda.
  •                self.lambda_func = experimental_lambdify(self.args, self.expr, use_python_cmath=True)
  •                self.vector_func = np.vectorize(self.lambda_func, otypes=[np.complex])
  •                results = np.real(self.call(*args))
  •                warnings.warn('Complex values encountered. Returning only the real part.')
  •            else:
  •                # Complete failure. One last try with no translations, only
  •                # wrapping in complex((...).evalf()) and returning the real
  •                # part.
  •                if self.failure:
  •                    raise e

Is there any particular reason why we are raising the exception. Repeated calls to to a vectorized_lambdify function will raise the exception when adaptive sampling.


Reply to this email directly or view it on GitHub:
https://github.com/sympy/sympy/pull/673/files#r874451

This commit
commit cb3bf49
Author: Chris Smith <smichr@gmail.com>

Has changed the way a certain error in __float__ is raised. This is a small
change to experimental_lambdify in order to accomodate the new behaviour in
master. Test are provided.
@Krastanov
Copy link
Member Author

🔴 There were test failures.

In this module there is syntax unsupported in 2.5

@Krastanov
Copy link
Member Author

There are more than one errors related to python2.5 (something about set.union that I do not understand). I won't be able to fix these during the weekend.

A keyword argument was used after unpacking of args. This was fixed by using
unpacking of kwargs.

set.union was called with variable number of arguments. In 2.5 only two
arguments are allowed. This was fixed by using reduce.
@Krastanov
Copy link
Member Author

The fixes for 2.5 are in. I am waiting for new bugs or a review/merge.

Calculator-like Interface
=========================

>>> p = Plot(visible=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this and the plots appeared but some error appeared, too:

>>> p=Plot(visible=False)
sympy\plotting\proxy_pyglet.py:30: SymPyDeprecationWarning:

This interface will change in future versions of sympy. As a
precatuion use the plot() function (lowercase). See the docstring for
details.

  SymPyDeprecationWarning)
>>> f=x**2
>>> p[1]=x**2
>>> p[2]=f.diff(x)
>>> p[3]=f.diff(x).diff(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "sympy\plotting\pygletplot\plot.py", line 301, in __setitem__
    f = PlotMode(*args, **kwargs)
  File "sympy\plotting\pygletplot\plot_mode.py", line 75, in __new__
    subcls = PlotMode._get_mode(mode_arg, i, d)
  File "sympy\plotting\pygletplot\plot_mode.py", line 153, in _get_mode
    return PlotMode._get_default_mode(i, d)
  File "sympy\plotting\pygletplot\plot_mode.py", line 167, in _get_default_mode
    return PlotMode._mode_default_map[d][i]
KeyError: 0
>>> p.show()
>>> p.clear()
>>> p
<blank plot>
>>> p[1]=x**2+y**2
>>> p[1].style='solid'
>>> p[2]=-x**2-y**2
>>> p[2].style='wireframe'
>>> p[1].color=z,(0.4,.4,.9),(.9,.4,.4)
>>> p[1].style='both'
>>> p[2].style='both'
>>> p.close()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smichr, You have been using the old plotting module that is working on pyglet. I have little idea how it works and I have not made changes to it (I have only moved it to a subfolder).

Check the docstrings of both from sympy import Plot (a proxy object to the old plotting module in order to have deprecation warnings) and from sympy import plot. Both of them should be imported with from sympy import *

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this branch still does not permit plotting constant
expressions... I won't have time to work on this during this week.

@smichr
Copy link
Member

smichr commented Jun 11, 2012

I rebased, fixed the typos I mentioned, and committed this from here after also fixing a whitespace error. Are there any issues to close?

@smichr smichr closed this Jun 11, 2012
@catchmrbharath
Copy link
Contributor

I guess the only issue is plotting constants. I haven't found an non - ugly way to do it.
Otherwise it is ready to be merged. It will be great if it gets merged so that I can send
the my gsoc work as pull requests and get it reviewed by the community.

@Krastanov
Copy link
Member Author

Wow, thanks Chris, I am grateful for the help. I will update/close the related issues.

@smichr
Copy link
Member

smichr commented Jun 11, 2012

On Mon, Jun 11, 2012 at 7:19 PM, Bharath M R
reply@reply.github.com
wrote:

I guess the only issue is plotting constants. I haven't found an non - ugly way to do it.
Otherwise it is ready to be merged. It will be great if it gets merged so that I can send
the my gsoc work as pull requests and get it reviewed by the community.

It's in. BTW, what is "plotting a constant"?

@mrocklin
Copy link
Member

Awesome. I think that an e-mail or blogpost should go out about this. The developer community should start using it to generate (and hopefully fix) some issues. Stefan, I know you've posted a few demos before in various places. Is there a single HTML page somewhere that documents how to do basic things with this module? Is there a good help page?

@catchmrbharath
Copy link
Contributor

@smichr
plot(1). This throws an error.

@smichr
Copy link
Member

smichr commented Jun 11, 2012

On Mon, Jun 11, 2012 at 8:26 PM, Bharath M R
reply@reply.github.com
wrote:

@smichr
plot(1). This throws an error.

What should it do? Is this suppose to give a y=1 graph?

@Krastanov
Copy link
Member Author

plot(1). This throws an error.

What should it do? Is this suppose to give a y=1 graph?

Yes. It makes sense for it to return a y=1 graph. However the
detection of free variables, etc depends on actually having some free
variables, thus this fails for the moment.

@Krastanov
Copy link
Member Author

Awesome. I think that an e-mail or blogpost should go out about this. The developer community should start using it to generate (and hopefully fix) some issues. Stefan, I know you've posted a few demos before in various places. Is there a single HTML page somewhere that documents how to do basic things with this module? Is there a good help page?

I will prepare something before the end of the day.

@Krastanov
Copy link
Member Author

Awesome. I think that an e-mail or blogpost should go out about this. The developer community should start using it to generate (and hopefully fix) some issues. Stefan, I know you've posted a few demos before in various places. Is there a single HTML page somewhere that documents how to do basic things with this module? Is there a good help page?

I will prepare something before the end of the day.

The help page (meaning the dosctring that is not imported in sphinx)
is detailed, however hard to read. There are notebooks with examples,
that are very good (I hope :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants