Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to plot a thermometer?

In a recent, very broad question it was asked how to plot several symbols, like "circles, squares, rectangles, stars, thermometers, and boxplots" with matplotlib. From that list, all but thermometers are obvious as either shown in the documentation or in many existing stackoverflow answers. Since the OP did not seem interested in thermomenters at all, I'd rather ask a new question specifically about thermometers here.

How to plot thermometers in matplotlib?

In principle you can plot any symbol you like, making it either a marker or a Path. There does not seem to be any unicode symbol for thermometers though. Font awesome has a thermometer symbol and plotting FontAwesome symbols in matplotlib is possible. Yet there are only 5 differnt fillings

enter image description here

Also, the color of such font symbol is uniform, yet ideally one would have the inner part of a thermometer (the "mercury pillar") in a different color (probably mostly red for associative reasons) or in different colors as to encode temperature in color as well.

So is it possible to have a temperature symbol where the mercury pillar encodes temperature (or in fact any other quantity) in terms of color and filling level? And if so, how?

(I gave an answer below, alternatives to or improvements of that method are welcome as further answers here.)

like image 843
ImportanceOfBeingErnest Avatar asked Feb 20 '19 15:02

ImportanceOfBeingErnest


People also ask

What is a thermometer chart called?

A thermometer chart (also known as a thermometer goal chart or progress chart) is a kind of progress chart that shows the current completed percentage of the task or the value of any metric relative to the predefined value. E.g., the thermometer scale can display the sales plan status or client satisfaction rate.


1 Answers

An option to plot a thermometer consisting of two parts is to create two Paths, the outer hull and the inner mercury pillar. For this one can create the Paths from scratch and allow the inner path to be variable depending on a (normalized) input parameter.

Then plotting both paths as individual scatter plots is possible. In the following, we create a class that has a scatter method, which works similar to a usual scatter, except that it would also take the additional arguments temp for the temperature and tempnorm for the normalization of the temperature as input.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath

class TemperaturePlot():

    @staticmethod
    def get_hull():
        verts1 = np.array([[0,-128],[70,-128],[128,-70],[128,0],
                          [128,32.5],[115.8,61.5],[96,84.6],[96,288],
                          [96,341],[53,384],[0,384]])
        verts2 = verts1[:-1,:] * np.array([-1,1])
        codes1 = [1,4,4,4,4,4,4,2,4,4,4]
        verts3 = np.array([[0,-80],[44,-80],[80,-44],[80,0],
                          [80,34.3],[60.7,52],[48,66.5],[48,288],
                          [48,314],[26.5,336],[0,336]])
        verts4 = verts3[:-1,:] * np.array([-1,1])
        verts = np.concatenate((verts1, verts2[::-1], verts4, verts3[::-1]))
        codes = codes1 + codes1[::-1][:-1]
        return mpath.Path(verts/256., codes+codes)

    @staticmethod
    def get_mercury(s=1):
        a = 0; b = 64; c = 35
        d = 320 - b
        e = (1-s)*d
        verts1 = np.array([[a,-b],[c,-b],[b,-c],[b,a],[b,c],[c,b],[a,b]])
        verts2 = verts1[:-1,:] * np.array([-1,1])
        verts3 = np.array([[0,0],[32,0],[32,288-e],[32,305-e],
                           [17.5,320-e],[0,320-e]])
        verts4 = verts3[:-1,:] * np.array([-1,1])
        codes = [1] + [4]*12 + [1,2,2,4,4,4,4,4,4,2,2]
        verts = np.concatenate((verts1, verts2[::-1], verts3, verts4[::-1]))
        return mpath.Path(verts/256., codes)

    def scatter(self, x,y, temp=1, tempnorm=None, ax=None, **kwargs):
        self.ax = ax or plt.gca()
        temp = np.atleast_1d(temp)
        ec = kwargs.pop("edgecolor", "black")
        kwargs.update(linewidth=0)
        self.inner = self.ax.scatter(x,y, **kwargs)
        kwargs.update(c=None, facecolor=ec, edgecolor=None, color=None)
        self.outer = self.ax.scatter(x,y, **kwargs)
        self.outer.set_paths([self.get_hull()])
        if not tempnorm:
            mi, ma = np.nanmin(temp), np.nanmax(temp)
            if mi == ma:
                mi=0
            tempnorm = plt.Normalize(mi,ma)
        ipaths = [self.get_mercury(tempnorm(t)) for t in temp]
        self.inner.set_paths(ipaths)

Usage of this class could look like this,

plt.rcParams["figure.figsize"] = (5.5,3) 
plt.rcParams["figure.dpi"] = 72*3

fig, ax = plt.subplots()
p = TemperaturePlot()
p.scatter([.25,.5,.75], [.3,.4,.5], s=[800,1200,1600], temp=[28,39,35], color="C3",
          ax=ax, transform=ax.transAxes)

plt.show()

where we plot 3 Thermometers with different temperatures depicted by the fill of the "mercury" pillar. Since no normalization is given it will normalize the temperatures of [28,39,35] between their minimum and maximum.

enter image description here

Or we can use color (c) and temp to show the temparature as in

np.random.seed(42)
fig, ax = plt.subplots()
n = 42
x = np.linspace(0,100,n)
y = np.cumsum(np.random.randn(n))+5

ax.plot(x,y, color="darkgrey", lw=2.5)

p = TemperaturePlot()
p.scatter(x[::4],y[::4]+3, s=300, temp=y[::4], c=y[::4], edgecolor="k", cmap="RdYlBu_r")

ax.set_ylim(-6,18)
plt.show()

enter image description here

like image 167
ImportanceOfBeingErnest Avatar answered Oct 27 '22 22:10

ImportanceOfBeingErnest