1

Overview

I have some data like so:

  • X values: range of frequencies
  • Y values: float value that can be positive or negative

I represent the data in a log-log plot. Since you can only take the logarithm of a positive number, I plot abs(Y-value).

On the log-log plot, I would like to represent the original number's sign by changing the marker symbol:

  • + marker if sign was +
  • - marker if sign was -

Generally: below I have placed my current method. I would like to "do it better". Hopefully matplotlib has a more standard way of doing this.

Details

Currently, here is what my plot looks like:

enter image description here

Here is some semblance of my current code (note: the data was pulled from equipment, so I just used random.uniform in this case):

import numpy as np
import matplotlib.pyplot as plt
from random import uniform


# Generating data
num_pts = 150
freq_arr = np.logspace(start=2, stop=6, num=num_pts, base=10)
reactance_arr = [uniform(-1000,1000) for i in range(num_pts)]
abs_reactance_arr = [abs(i) for i in reactance_arr]
reactance_signed_marker = [1 if reactance_arr[i] >= 0 else -1 for i in range(len(reactance_arr))]

# Taken from here: https://stackoverflow.com/questions/28706115/how-to-use-different-marker-for-different-point-in-scatter-plot-pylab
x = np.array(freq_arr)
y = np.array(abs_reactance_arr)
grouping = np.array(reactance_signed_marker)

# Plotting
fig1, ax1 = plt.subplots()

positives_line = ax1.scatter(
    x[grouping == 1], 
    y[grouping == 1], 
    s=16, 
    marker="+", 
    label="Reactance",
)

# Match color between the two plots
col = positives_line.get_facecolors()[0].tolist()

ax1.scatter(
    x[grouping == -1],
    y[grouping == -1],
    s=16,
    marker="_",
    label="Reactance",
    color=col,
)

ax1.set_xlim([freq_arr[0], freq_arr[-1]])
ax1.set_xscale("log")
ax1.set_xlabel("Frequency (Hz)")
ax1.set_yscale("log")
ax1.set_ylabel("Value (Ohm)")
ax1.legend()
ax1.set_title("Reactance")

How can I do this better? Currently, this feels very manual. I am wondering:

  • Is there a better way to parse - and + values into markers?
    • The current way is quite cumbersome with 1. plotting +, 2. extracting color, 3. plotting - with the same color
  • I would like the legend to be one united category
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119

3 Answers3

3

I proposed a scatter with multiple markers in iterating markers in plots. Applying it here would look like:

import numpy as np
import matplotlib.pyplot as plt

def mscatter(x, y, ax=None, m=None, **kw):
    import matplotlib.markers as mmarkers
    if not ax: ax=plt.gca()
    sc = ax.scatter(x,y,**kw)
    if (m is not None) and (len(m)==len(x)):
        paths = []
        for marker in m:
            if isinstance(marker, mmarkers.MarkerStyle):
                marker_obj = marker
            else:
                marker_obj = mmarkers.MarkerStyle(marker)
            path = marker_obj.get_path().transformed(
                        marker_obj.get_transform())
            paths.append(path)
        sc.set_paths(paths)
    return sc

# Generating data
num_pts = 150
freq_arr = np.logspace(start=2, stop=6, num=num_pts, base=10)
reactance_arr = np.random.uniform(-1000,1000,num_pts)

x = np.array(freq_arr)
y = np.abs(reactance_arr)
markers = np.array(["_", "+"])[(reactance_arr >= 0).astype(int)]

# Plotting
fig1, ax1 = plt.subplots()

mscatter(x, y, ax=ax1, s=16, m = markers, label="Reactance")

ax1.set_xlim([freq_arr[0], freq_arr[-1]])
ax1.set_xscale("log")
ax1.set_xlabel("Frequency (Hz)")
ax1.set_yscale("log")
ax1.set_ylabel("Value (Ohm)")
ax1.legend()
ax1.set_title("Reactance")

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • how does `markers = np.array(["_", "+"])[(reactance_arr >= 0).astype(int)]` work? I get what `np.array(["_", "+"])` and `[(reactance_arr >= 0).astype(int)]` do on their own, but combined I am not sure what's happening – Intrastellar Explorer Jul 24 '19 at 02:11
  • Maybe just print out `np.array(["A", "B"])[[1,0,0,1,1]]`... it will show B, A, A, B, B, namely the second, first, first, second, second element of the string array. – ImportanceOfBeingErnest Jul 24 '19 at 02:17
  • Yeah I tried that, and it helps. What is this operation called @ImportanceOfBeingErnest? You have a `np.ndarray` then a 1 item list of `np.ndarray`. Is this considered slicing? – Intrastellar Explorer Jul 24 '19 at 02:22
  • 1
    No, there is no slice involved. I think numpy calls it [Advanced indexing](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html#integer-array-indexing) – ImportanceOfBeingErnest Jul 24 '19 at 02:47
2

I don't think matplotlib scatter supports different markers in a single call, so the method you are using is correct. However, the plotting part of your code could be shortened, if that's what you want. You can set the labels as you did before.

fig1, ax1 = plt.subplots()
markers = ["_","+"]
for i, c in enumerate(np.unique(grouping)):
    ax1.scatter(x[grouping==c],y[grouping==c],marker=markers[i],label='Reactance',c='tab:blue')

(I just noticed a new answer as I was planning to post, sorry if this seems repetitive)

zsheryl
  • 73
  • 7
1

I'm not aware of a matplotlib functionality that does what you want. However, based on your initial code, I made some clean up to improve it, especially to use numpy rather than list comprehensions. The legend now has only one entry (btw, which marker would you like to have displayed in the legend?). I set the marker colors to different values because I think it provides a better distinction, but you can change it easily inside the scatter function (c parameter).

import numpy as np
import matplotlib.pyplot as plt
from numpy.random import uniform

# Generating data
num_pts = 150
freq_arr = np.logspace(start=2, stop=6, num=num_pts, base=10)
reactance_arr = uniform(-1000, 1000, size=num_pts)

# Plotting
fig, ax = plt.subplots()

pos_idx = reactance_arr >= 0
ax.scatter(freq_arr[pos_idx], np.abs(reactance_arr)[pos_idx], s=16,
           c='tab:blue', marker="+", label="Reactance")
neg_idx = reactance_arr < 0
ax.scatter(freq_arr[neg_idx], np.abs(reactance_arr)[neg_idx], s=16, 
           c='tab:orange', marker="_")

ax.set(xlim=[freq_arr[0], freq_arr[-1]], xscale="log", xlabel="Frequency (Hz)",
       yscale="log", ylabel="Value (Ohm)", title="Reactance")
ax.legend()
afb
  • 253
  • 3
  • 7
  • thank you for showing me how to compare with `np.array`, also how to use `ax.set`, these cleaned up my code quite a bit! If I were to pick a marker to have in the legend, I would probably choose `+`, as you have – Intrastellar Explorer Jul 24 '19 at 02:02