Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions holoviews/plotting/mpl/heatmap.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from itertools import product
from itertools import product # noqa F401 # don't remove for now

import numpy as np
import param
Expand Down Expand Up @@ -81,19 +81,27 @@ def _annotate_plot(self, ax, annotations):

def _annotate_values(self, element, xvals, yvals):
val_dim = element.vdims[0]
vals = element.dimension_values(val_dim).flatten()
vals = element.dimension_values(val_dim, flat=False)
xpos = xvals[:-1] + np.diff(xvals)/2.
ypos = yvals[:-1] + np.diff(yvals)/2.
plot_coords = product(xpos, ypos)

# When invert_axes=True, `get_data` transposes and reverses both axes.
# Apply the same to the annotation values.
if self.invert_axes:
vals = vals.T[::-1, ::-1]

annotations = {}
for plot_coord, v in zip(plot_coords, vals, strict=None):
text = '-' if is_nan(v) else val_dim.pprint_value(v)
annotations[plot_coord] = text
for j, y in enumerate(ypos):
for i, x in enumerate(xpos):
v = vals[j, i]
text = '-' if is_nan(v) else val_dim.pprint_value(v)
annotations[(x, y)] = text
return annotations


def _compute_ticks(self, element, xvals, yvals, xfactors, yfactors):
xdim, ydim = element.kdims
aggregate = element.gridded
if self.invert_axes:
xdim, ydim = ydim, xdim

Expand All @@ -103,17 +111,20 @@ def _compute_ticks(self, element, xvals, yvals, xfactors, yfactors):
if xticks is None:
xpos = xvals[:-1] + np.diff(xvals)/2.
if not xfactors:
xfactors = element.gridded.dimension_values(xdim, False)
xfactors = aggregate.dimension_values(xdim, False)
xlabels = [xdim.pprint_value(k) for k in xfactors]
xticks = list(zip(xpos, xlabels, strict=None))

yticks = opts.get('yticks')
if yticks is None:
ypos = yvals[:-1] + np.diff(yvals)/2.
if not yfactors:
yfactors = element.gridded.dimension_values(ydim, False)
yfactors = aggregate.dimension_values(ydim, False)
ylabels = [ydim.pprint_value(k) for k in yfactors]
yticks = list(zip(ypos, ylabels, strict=None))
ytype = aggregate.interface.dtype(aggregate, ydim)
if ytype.kind in 'SUO':
positions = ypos[::-1]
yticks = list(zip(positions, ylabels, strict=None))
return xticks, yticks


Expand Down Expand Up @@ -181,7 +192,7 @@ def get_data(self, element, ranges, style):
style['yfactors'] = yfactors

if self.show_values:
style['annotations'] = self._annotate_values(element.gridded, xvals, yvals)
style['annotations'] = self._annotate_values(aggregate, xvals, yvals)
vdim = element.vdims[0]
self._norm_kwargs(element, ranges, style, vdim)
if 'vmin' in style:
Expand Down
84 changes: 84 additions & 0 deletions holoviews/tests/plotting/matplotlib/test_heatmapplot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pandas as pd

from holoviews.element import HeatMap, Image

Expand Down Expand Up @@ -44,3 +45,86 @@
expected = np.array([1, np.inf, np.inf, 2])
masked = np.ma.array(expected, mask=np.logical_not(np.isfinite(expected)))
np.testing.assert_equal(array, masked)

def test_heatmap_categorical_factors_preserve_appearance_and_Z_edges(self):
"""Categorical x/y should preserve first-seen order."""
data = pd.DataFrame(
{
"X": ["L", "N", "O", "L", "N", "L", "N", "M"],
"Y": ["C", "C", "C", "B", "B", "A", "A", "A"],
"count": [301, 37, 2, 212, 8, 34, 1, 1],
}
)
# Categorical/object dtypes: appearance order should be preserved
hmap = HeatMap(data, ["X", "Y"]).aggregate(function=np.mean)
agg = hmap.aggregate(function=np.mean).gridded
xdim, ydim = agg.dimensions(label=True)[:2]

Z = agg.dimension_values(2, flat=False)
Z = np.ma.array(Z, mask=np.logical_not(np.isfinite(Z)))

# Expected factors: first-seen order in input rows
expected_x = ["L", "N", "O", "M"]
expected_y = ["A", "B", "C"]

assert list(agg.dimension_values(xdim, False)) == expected_x
assert list(agg.dimension_values(ydim, False)) == expected_y

# Expected Z edges match first and last y rows
assert Z[0].tolist() == [34.0, 1.0, None, 1.0]
assert Z[-1].tolist() == [301.0, 37.0, 2.0, None]

def test_heatmap_categorical_factors_preserve_appearance_with_inverted_input(self):
"""Changing first-seen order in the input should change factors and Z edges accordingly."""
inv_data = pd.DataFrame(
{
"X": ["M", "N", "L", "N", "L", "O", "N", "L"],
"Y": ["A", "A", "A", "B", "B", "C", "C", "C"],
"count": [1, 1, 34, 8, 212, 2, 37, 301],
}
)

hmap = HeatMap(inv_data, ["X", "Y"]).aggregate(
function=np.mean
)
agg = hmap.aggregate(function=np.mean).gridded
xdim, ydim = agg.dimensions(label=True)[:2]

Z = agg.dimension_values(2, flat=False)
Z = np.ma.array(Z, mask=np.logical_not(np.isfinite(Z)))

# Expected factors: first-seen order in input rows
expected_x = ["M", "N", "L", "O"]
expected_y = ["C", "B", "A"]

assert list(agg.dimension_values(xdim, False)) == expected_x
assert list(agg.dimension_values(ydim, False)) == expected_y

Check failure on line 101 in holoviews/tests/plotting/matplotlib/test_heatmapplot.py

View workflow job for this annotation

GitHub Actions / unit:test-313:ubuntu-latest

TestHeatMapPlot.test_heatmap_categorical_factors_preserve_appearance_with_inverted_input AssertionError: assert ['A', 'B', 'C'] == ['C', 'B', 'A'] At index 0 diff: 'A' != 'C' Full diff: [ + 'A', + 'B', 'C', - 'B', - 'A', ]

Check failure on line 101 in holoviews/tests/plotting/matplotlib/test_heatmapplot.py

View workflow job for this annotation

GitHub Actions / unit:test-313:macos-latest

TestHeatMapPlot.test_heatmap_categorical_factors_preserve_appearance_with_inverted_input AssertionError: assert ['A', 'B', 'C'] == ['C', 'B', 'A'] At index 0 diff: 'A' != 'C' Full diff: [ + 'A', + 'B', 'C', - 'B', - 'A', ]

Check failure on line 101 in holoviews/tests/plotting/matplotlib/test_heatmapplot.py

View workflow job for this annotation

GitHub Actions / unit:test-310:ubuntu-latest

TestHeatMapPlot.test_heatmap_categorical_factors_preserve_appearance_with_inverted_input AssertionError: assert ['A', 'B', 'C'] == ['C', 'B', 'A'] At index 0 diff: 'A' != 'C' Full diff: [ + 'A', + 'B', 'C', - 'B', - 'A', ]

Check failure on line 101 in holoviews/tests/plotting/matplotlib/test_heatmapplot.py

View workflow job for this annotation

GitHub Actions / unit:test-310:macos-latest

TestHeatMapPlot.test_heatmap_categorical_factors_preserve_appearance_with_inverted_input AssertionError: assert ['A', 'B', 'C'] == ['C', 'B', 'A'] At index 0 diff: 'A' != 'C' Full diff: [ + 'A', + 'B', 'C', - 'B', - 'A', ]

# Expected Z edges match first and last y rows
assert Z[0].tolist() == [301.0, 37.0, 2.0, None]
assert Z[-1].tolist() == [34.0, 1.0, None, 1.0]

def test_heatmap_numeric_axes_sorted_and_Z_edges(self):
"""Numeric axes should be sorted ascending; verify factors and Z edges."""
df = pd.DataFrame(
{
"x": [2, 1, 3, 1],
"y": [200, 100, 300, 100],
"val": [ 20, 10, 30, 11],
}
)
hmap = HeatMap(df, ["x", "y"]).aggregate(function=np.mean)
agg = hmap.aggregate(function=np.mean).gridded
xdim, ydim = agg.dimensions(label=True)[:2]

Z = agg.dimension_values(2, flat=False)
Z = np.ma.array(Z, mask=np.logical_not(np.isfinite(Z)))

# Numeric factors sorted ascending
assert list(agg.dimension_values(xdim, False)) == [1, 2, 3]
assert list(agg.dimension_values(ydim, False)) == [100, 200, 300]

# First row corresponds to min y=100; last row to max y=300
# Columns correspond to x=[1,2,3]
np.testing.assert_equal( Z[0], np.array([10.5, np.nan, np.nan]) ) # mean of (10,11)
np.testing.assert_equal(Z[-1], np.array([np.nan, np.nan, 30]))
Loading