Skip to content
Open
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
146 changes: 69 additions & 77 deletions geemap/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


class DataTable(pd.DataFrame):
"""DataFrame that can be initialized from EE objects."""

def __init__(
self,
Expand Down Expand Up @@ -168,7 +169,7 @@ def array_to_df(
return pd.DataFrame(data, **kwargs)


class Chart:
class Chart: # pylint: disable=too-many-instance-attributes
"""Create and display various types of charts from a data table.

Attributes:
Expand Down Expand Up @@ -466,7 +467,7 @@ def set_options(self, **options: Any) -> None:
setattr(self.figure, key, value)


class BaseChartClass:
class BaseChartClass: # pylint: disable=too-many-instance-attributes
"""This should include everything a chart module requires to plot figures."""

def __init__(
Expand Down Expand Up @@ -525,15 +526,11 @@ def __init__(
for key, value in kwargs.items():
setattr(self, key, value)

@classmethod
def get_data(cls) -> None:
def get_data(self) -> None:
"""Placeholder method to get data for the chart."""
pass

@classmethod
def plot_chart(cls) -> None:
def plot_chart(self) -> None:
"""Placeholder method to plot the chart."""
pass

def __repr__(self) -> str:
"""Returns the string representation of the chart."""
Expand All @@ -551,7 +548,7 @@ def __init__(
features: ee.FeatureCollection | pd.DataFrame,
default_labels: list[str],
name: str,
type: str = "grouped",
type: str = "grouped", # pylint: disable=redefined-builtin
**kwargs: Any,
):
"""A BarChart with the given features, labels, name, and type.
Expand All @@ -565,6 +562,10 @@ def __init__(
"""
super().__init__(features, default_labels, name, **kwargs)
self.type: str = type
self.x_data = None
self.y_data = None
self.yProperty = None
self.bar_chart = None

def generate_tooltip(self) -> None:
"""Generates a tooltip for the bar chart."""
Expand Down Expand Up @@ -651,6 +652,7 @@ def __init__(
**kwargs: Additional keyword arguments to set as attributes.
"""
super().__init__(features, labels, name, **kwargs)
self.line_chart = None

def plot_chart(self) -> None:
"""Plots the line chart."""
Expand Down Expand Up @@ -702,20 +704,14 @@ def __init__(
"""
default_labels = y_properties
super().__init__(features, default_labels, name, **kwargs)
self.x_data, self.y_data = self.get_data(x_property, y_properties)

def get_data(
self, x_property: str, y_properties: list[str]
) -> tuple[list[Any], list[Any]]:
"""Returns the x and y data for the chart.

Args:
x_property: The property to use for the x-axis.
y_properties: The properties to use for the y-axis.
"""
x_data = list(self.df[x_property])
y_data = list(self.df[y_properties].values.T)

self.x_property = x_property
self.y_properties = y_properties
self.x_data, self.y_data = self.get_data()

def get_data(self) -> tuple[list[Any], list[Any]]:
"""Returns the x and y data for the chart."""
x_data = list(self.df[self.x_property])
y_data = list(self.df[self.y_properties].values.T)
return x_data, y_data


Expand All @@ -740,37 +736,33 @@ def __init__(
**kwargs: Additional keyword arguments to set as attributes.

Raises:
Exception: If 'labels' is in kwargs.
ValueError: If 'labels' is in kwargs.
"""
default_labels = None
super().__init__(
features, default_labels, name, **kwargs
) # pytype: disable=wrong-arg-types
if "labels" in kwargs:
raise Exception("Please remove labels in kwargs and try again.")
raise ValueError("Please remove labels in kwargs and try again.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a behavior change. I'm okay with that if you are.


self.labels = list(self.df[series_property])
self.x_data, self.y_data = self.get_data(x_properties)
self.x_properties = x_properties
self.x_data, self.y_data = self.get_data()

def get_data(
self, x_properties: list[str] | dict[str, str]
) -> tuple[list[Any], list[Any]]:
def get_data(self) -> tuple[list[Any], list[Any]]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This changes the API. While it seems right to do in the long run, I think we should hold off doing that during the initial linting phase.

"""Returns the x and y data for the chart.

Args:
x_properties: The properties to use for the x-axis.

Raises:
Exception: If x_properties is not a list or dictionary.
TypeError: If x_properties is not a list or dictionary.
"""
if isinstance(x_properties, list):
x_data = x_properties
y_data = self.df[x_properties].values
elif isinstance(x_properties, dict):
x_data = list(x_properties.values())
y_data = self.df[list(x_properties.keys())].values
if isinstance(self.x_properties, list):
x_data = self.x_properties
y_data = self.df[self.x_properties].values
elif isinstance(self.x_properties, dict):
x_data = list(self.x_properties.values())
y_data = self.df[list(self.x_properties.keys())].values
else:
raise Exception("x_properties must be a list or dictionary.")
raise TypeError("x_properties must be a list or dictionary.")

return x_data, y_data

Expand All @@ -785,7 +777,7 @@ def __init__(
y_property: str,
series_property: str,
name: str = "feature.groups",
type: str = "stacked",
type: str = "stacked", # pylint: disable=redefined-builtin
**kwargs: Any,
):
"""Initialize a Feature_Groups.
Expand All @@ -805,8 +797,9 @@ def __init__(
self.yProperty = y_property
super().__init__(features, default_labels, name, type, **kwargs)

self.x_property = x_property
self.new_column_names = self.get_column_names(series_property, y_property)
self.x_data, self.y_data = self.get_data(x_property, self.new_column_names)
self.x_data, self.y_data = self.get_data()

def get_column_names(self, series_property: str, y_property: str) -> list[str]:
"""Returns the new column names for the DataFrame.
Expand All @@ -825,17 +818,10 @@ def get_column_names(self, series_property: str, y_property: str) -> list[str]:

return new_column_names

def get_data(
self, x_property: str, new_column_names: list[str]
) -> tuple[list[Any], list[Any]]:
"""Returns the x and y data for the chart.

Args:
x_property: The property to use for the x-axis.
new_column_names: The new column names for the y-axis.
"""
x_data = list(self.df[x_property])
y_data = [self.df[x] for x in new_column_names]
def get_data(self) -> tuple[list[Any], list[Any]]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another API change

"""Returns the x and y data for the chart."""
x_data = list(self.df[self.x_property])
y_data = [self.df[x] for x in self.new_column_names]

return x_data, y_data

Expand All @@ -859,11 +845,14 @@ def feature_by_feature(
y_properties: Values of y_properties.
**kwargs: Additional keyword arguments to set as attributes.
"""
bar = Feature_ByFeature(
features=features, x_property=x_property, y_properties=y_properties, **kwargs
chart = Feature_ByFeature(
features=features,
x_property=x_property,
y_properties=y_properties,
**kwargs,
)

bar.plot_chart()
chart.plot_chart()


def feature_by_property(
Expand All @@ -887,14 +876,14 @@ def feature_by_property(
series_property (str): The name of the property used to label each
feature in the legend.
"""
bar = Feature_ByProperty(
chart = Feature_ByProperty(
features=features,
x_properties=x_properties,
series_property=series_property,
**kwargs,
)

bar.plot_chart()
chart.plot_chart()


def feature_groups(
Expand All @@ -919,20 +908,20 @@ def feature_groups(
**kwargs: Additional keyword arguments to set as attributes.
"""

bar = Feature_Groups(
chart = Feature_Groups(
features=features,
x_property=x_property,
y_property=y_property,
series_property=series_property,
**kwargs,
)

bar.plot_chart()
chart.plot_chart()


def feature_histogram(
features: ee.FeatureCollection,
property: str,
property: str, # pylint: disable=redefined-builtin
max_buckets: int | None = None,
min_bucket_width: float | None = None,
show: bool = True,
Expand All @@ -958,20 +947,21 @@ def feature_histogram(
**kwargs: Additional keyword arguments to set as attributes.

Raises:
Exception: If the provided xProperties is not a list or dict.
Exception: If the chart fails to create.
TypeError: If features is not an ee.FeatureCollection.
ValueError: If property is not found in features.

Returns:
The bqplot chart object if show is False, otherwise None.
"""
if not isinstance(features, ee.FeatureCollection):
raise Exception("features must be an ee.FeatureCollection")
raise TypeError("features must be an ee.FeatureCollection")

first = features.first()
props = first.propertyNames().getInfo()
if property not in props:
raise Exception(
f"property {property} not found. Available properties: {', '.join(props)}"
raise ValueError(
f"property {property} not found. Available properties:"
f" {', '.join(props)}"
)

def nextPowerOf2(n) -> float:
Expand Down Expand Up @@ -1225,9 +1215,9 @@ def group_by_doy(collection, start, end, reducer):
)

# Group images by their day of year.
filter = ee.Filter(ee.Filter.equals(leftField="doy", rightField="doy"))
doy_filter = ee.Filter.equals(leftField="doy", rightField="doy")
joined = ee.Join.saveAll("matches").apply(
primary=doys, secondary=collection, condition=filter
primary=doys, secondary=collection, condition=doy_filter
)

# For each DoY, reduce images across years.
Expand Down Expand Up @@ -1360,9 +1350,9 @@ def group_by_doy(collection, start, end, reducer):
)

# Group images by their day of year.
filter = ee.Filter(ee.Filter.equals(leftField="doy", rightField="doy"))
doy_filter = ee.Filter.equals(leftField="doy", rightField="doy")
joined = ee.Join.saveAll("matches").apply(
primary=doys, secondary=collection, condition=filter
primary=doys, secondary=collection, condition=doy_filter
)

# For each DoY, reduce images across years.
Expand Down Expand Up @@ -1510,12 +1500,12 @@ def create_feature(image):
distinct_doy_year = tuples.distinct(["doy", "year"])

# Join the original tuples with the distinct (doy, year) pairs.
filter = ee.Filter.And(
doy_filter = ee.Filter.And(
ee.Filter.equals(leftField="doy", rightField="doy"),
ee.Filter.equals(leftField="year", rightField="year"),
)
joined = ee.Join.saveAll("matches").apply(
primary=distinct_doy_year, secondary=tuples, condition=filter
primary=distinct_doy_year, secondary=tuples, condition=doy_filter
)

# For each (doy, year), reduce the values of the joined features.
Expand Down Expand Up @@ -1552,7 +1542,7 @@ def image_histogram(
min_bucket_width: float,
max_raw: int,
max_pixels: int,
reducer_args: dict[str, Any] = {},
reducer_args: dict[str, Any] | None = None,
**kwargs: dict[str, Any],
) -> bq.Figure:
"""Creates a histogram for each band of the specified image within the given region.
Expand All @@ -1576,6 +1566,8 @@ def image_histogram(
Returns:
The bqplot figure containing the histograms.
"""
if reducer_args is None:
reducer_args = {}
# Calculate the histogram data.
histogram = image.reduceRegion(
reducer=ee.Reducer.histogram(
Expand Down Expand Up @@ -1611,7 +1603,7 @@ def create_histogram(
x_sc = bq.LinearScale()
y_sc = bq.LinearScale()

bar = bq.Bars(
chart = bq.Bars(
x=x_data,
y=y_data,
scales={"x": x_sc, "y": y_sc},
Expand All @@ -1625,7 +1617,7 @@ def create_histogram(
scale=y_sc, orientation="vertical", label="Count", tick_format="0.0f"
)

return bq.Figure(marks=[bar], axes=[ax_x, ax_y])
return bq.Figure(marks=[chart], axes=[ax_x, ax_y])

# Define colors and labels for the bands.
band_colors = kwargs.get("colors", ["#cf513e", "#1d6b99", "#f0af07"])
Expand Down Expand Up @@ -1737,7 +1729,7 @@ def get_stats(image):
for band in band_names:
results[band] = stats.get(band)

if x_property == "system:time_start" or x_property == "system:time_end":
if x_property in ("system:time_start", "system:time_end"):
results["date"] = image.date().format("YYYY-MM-dd")
else:
results[x_property] = image.get(x_property).getInfo()
Expand Down Expand Up @@ -1829,7 +1821,7 @@ def image_series_by_region(
df = df.drop(columns=[series_property]).T
df.columns = headers

if x_property == "system:time_start" or x_property == "system:time_end":
if x_property in ("system:time_start", "system:time_end"):
indexes = common.image_dates(image_collection).getInfo()
df["index"] = pd.to_datetime(indexes)

Expand Down