Davide-sd / sympy-plot-backends

An improved plotting module for SymPy
BSD 3-Clause "New" or "Revised" License
42 stars 9 forks source link

plot_piecewise adds duplicate labels in legend #32

Closed sadguitarius closed 5 months ago

sadguitarius commented 8 months ago

Hi @Davide-sd, thanks for the great progress on this library! I wanted to bring up a small issue. When plotting a function with plot_piecewise and adding a legend, I get a duplicate label for each individual graphical element. This doesn't seem like desired behavior. Here's a code snippet to illustrate. I'd be happy to help troubleshoot if this doesn't seem like an easy fix and you don't have the time. Thank you!

import spb
import sympy as sym
fn = sym.Piecewise((1, sym.Eq(a, 0)), (sym.asinh(a)/a, True))
spb.plot_piecewise(fn, legend=True, label=['piecewise function'])

output

Davide-sd commented 8 months ago

Hello @sadguitarius ,

thank you for opening the issue. I'm going to explain how plot_piecewise works (which is a little bit different than other plot functions) and what you can do to achieve your desired result.

plot_piecewise is going to extract pieces from piecewise functions. Let's consider your example: plot_piecewise creates 5 different series from which it will extract numerical data:

import spb
import sympy as sym
from sympy import *
sym.var("a x")
fn = sym.Piecewise((1, sym.Eq(a, 0)), (sym.asinh(a)/a, True))
p = spb.plot_piecewise(fn, legend=True)
for s in p:
    msg = str(s)
    if isinstance(s, spb.series.List2DSeries):
        msg += " " + str(s.get_data())
    print(msg)

Output:

cartesian line: asinh(a)/a for a over (-10.0, -1e-06)
2D list plot [array([-1.e-06]), array([1.])]
cartesian line: asinh(a)/a for a over (1e-06, 10.0)
2D list plot [array([1.e-06]), array([1.])]
2D list plot [array([0.]), array([1.])]

Let's zoom in around 0 in order to see all series being rendered:

spb.plot_piecewise(fn, xlim=(-1e-05, 1e-05))

image

The solid lines represent sym.asinh(a)/a. The empty circles represent the extreme values. Ideally, they should be located at x=0. Practically, they must be placed at some small epsilon (1e-06) away from the "breaking point", in order for the function to properly evaluate. For example: (sym.asinh(a)/a).subs(a, 0) gives NaN, which would prevent the dots from showing up on the plot. On the other hand, (sym.asinh(a)/a).subs(a, 1e-06) returns a numeric value.

When you execute spb.plot_piecewise(fn, legend=True), you'll get this: image By default, each symbolic expression is assigned a default label. The legend shows two labels because the plot contains two lines. By default, there are no labels associated to points.

When you execute spb.plot_piecewise(fn, legend=True, label=['piecewise function']), plot_piecewise sees one label, but 5 data series are present. In order to avoid raising errors, it copies the provided label to each data series. You end up with this: image

I believe you only want one entry legend for your function. You can do it in two steps. First, create a hidden plot. Then, set the labels and show it:

p = spb.plot_piecewise(fn, legend=True, show=False, label="test")
for i, s in enumerate(p):
    if i != 0:
        s.label = ""
p.show()

image

plot_piecewise definitely needs a better way to set labels. I don't have time to work on this right now, so I'll leave the issue open and fix it sometime in the future. Thanks for reporting it.

sadguitarius commented 8 months ago

Thanks for the detailed explanation! The workaround is totally doable. It's nice knowing what's going on under the hood, but it would be good to have a cleaner way to do this and one that conforms to expected behavior. Totally not a priority for now though! I'll check out the code if I get time too and see if I can figure anything out.

Davide-sd commented 5 months ago

New version is out, fixing this problem.

from sympy import *
from sympy.abc import a, x
from spb import *

fn = Piecewise((1, Eq(a, 0)), (asinh(a)/a, True))
plot_piecewise(fn, legend=True, label=['piecewise function'])

image

plot_piecewise(
    Heaviside(x, 0).rewrite(Piecewise),
    Piecewise(
        (sin(x), x < -5),
        (cos(x), x > 5),
        (1 / x, True)),
    ylim=(-2, 2), detect_poles=True, legend=True, label=["A", "B"])

image

I'm closing this issue.

sadguitarius commented 4 months ago

This is awesome, thanks so much!