from __future__ import annotations
import json
import random
from typing import TYPE_CHECKING, Any, cast
from django import template
from django.http import JsonResponse
from wildewidgets.views import WidgetInitKwargsMixin
from ..base import Widget
if TYPE_CHECKING:
from django.http.request import HttpRequest
[docs]class ApexDatasetBase(Widget):
"""
Base class for Apex chart datasets.
This class provides the foundation for defining datasets that can be added to
ApexCharts. It handles the transformation of Python data into the format
expected by the ApexCharts JavaScript library.
Attributes:
name: The name of the dataset, displayed in legends and tooltips
chart_type: The specific chart type for this dataset (e.g., 'line', 'bar')
data: The list of data points for this dataset
Example:
.. code-block:: python
class MyDataset(ApexDatasetBase):
name = "Monthly Sales"
chart_type = "line"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = [10, 41, 35, 51, 49, 62, 69, 91, 148]
Or use subclassing to customize the dataset further:
.. code-block:: python
class CustomDataset(ApexDatasetBase):
name = "Custom Data"
chart_type = "bar"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = self.load_custom_data()
def load_custom_data(self) -> list[int]:
# Load or generate custom data here
return [5, 15, 25, 35, 45]
"""
name: str | None = None
chart_type: str | None = None
def __init__(self, **kwargs: Any) -> None: # noqa: ARG002
"""
Initialize the dataset with empty data.
Args:
**kwargs: Additional keyword arguments (not used in base class)
"""
self.data: list[Any] = []
[docs] def get_options(self) -> dict[str, Any]:
"""
Get the dataset options in the format expected by ApexCharts.
This method transforms the dataset's properties into a dictionary format
that can be serialized to JSON and used by the ApexCharts JavaScript library.
Returns:
dict: The dataset configuration options
"""
options: dict[str, Any] = {"data": self.data}
if self.name:
options["name"] = self.name if self.name is not None else ""
if self.chart_type:
options["type"] = self.chart_type
return options
[docs]class ApexJSONMixin:
"""
A mixin class adding AJAX support to Apex Charts.
This mixin enables Apex charts to load their data asynchronously via AJAX.
It handles the HTTP request/response cycle and provides methods for
preparing the chart data as JSON.
Attributes:
template_name: The Django template file for rendering the AJAX-enabled chart
chart_options: Dictionary containing the chart configuration
Note:
Classes using this mixin should implement the `load` method to populate
the chart's datasets.
"""
template_name: str = "wildewidgets/apex_json.html"
chart_options: dict[str, Any]
[docs] def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse:
"""
Dispatch the request to the appropriate handler method.
This method delegates to the appropriate HTTP method handler (e.g., get, post)
based on the request method.
Args:
request: The HTTP request object
*args: Additional positional arguments
Keyword Arguments:
**kwargs: Additional keyword arguments
Returns:
JsonResponse: The JSON response containing chart data
"""
handler = getattr(self, cast("str", request.method).lower())
return handler(request, *args, **kwargs)
[docs] def get(self, _: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: # noqa: ARG002
"""
Handle GET requests for chart data.
This method is called when the client makes an AJAX GET request for
chart data. It returns the chart's series data as JSON.
Args:
_: The HTTP request object (unused)
*args: Additional positional arguments
**kwargs: Additional keyword arguments
Returns:
JsonResponse: The JSON response containing chart data
"""
data = self.get_series_data()
return self.render_to_response(data)
[docs] def render_to_response(
self,
context: dict[str, Any],
**response_kwargs: Any, # noqa: ARG002
) -> JsonResponse:
"""
Render the chart data as a JSON response.
Args:
context: The chart data to be serialized as JSON
Keyword Arguments:
**response_kwargs: Additional keyword arguments (unused)
Returns:
JsonResponse: The JSON response containing chart data
"""
return JsonResponse(context)
[docs] def get_series_data(self, **kwargs: Any) -> dict[str, Any]:
"""
Get the chart series data.
This method loads the chart data and adds the series to the provided
keyword arguments.
Keyword Args:
**kwargs: Keyword arguments to update with series data
Returns:
dict: The updated keyword arguments containing series data
"""
self.load()
kwargs["series"] = self.chart_options["series"]
return kwargs
[docs] def load(self) -> None:
"""
Load datasets into the chart via AJAX.
This method should be overridden by subclasses to populate the chart's
datasets. It will be called when an AJAX request is made for chart data.
Example:
.. code-block:: python
def load(self) -> None:
dataset = MyDataset()
dataset.data = [10, 41, 35, 51, 49, 62, 69, 91, 148]
self.add_dataset(dataset)
"""
[docs]class ApexChartBase(WidgetInitKwargsMixin):
"""
Base class for Apex Charts in Django applications.
This class provides the foundation for creating interactive Apex charts
in Django applications. It handles chart configuration, dataset management,
and rendering.
Attributes:
template_name: The Django template file for rendering the chart
css_id: The HTML ID attribute for the chart container
chart_type: The type of chart (e.g., 'line', 'bar', 'pie')
chart_options: Dictionary containing all chart configuration options
Example:
.. code-block:: python
class MyLineChart(ApexChartBase):
chart_type = 'line'
def __init__(self, **kwargs):
super().__init__(**kwargs)
dataset = MyDataset()
dataset.data = [10, 41, 35, 51, 49, 62, 69, 91, 148]
self.add_dataset(dataset)
self.add_categories(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep'])
"""
template_name: str = "wildewidgets/apex_chart.html"
css_id: str | int | None = None
chart_type: str | None = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Initialize the Apex chart.
Args:
*args: Positional arguments passed to parent class
Keyword Args:
**kwargs: Keyword arguments
css_id: Optional HTML ID for the chart container
"""
super().__init__(*args, **kwargs)
self.chart_options: dict[str, Any] = {}
self.chart_options["series"] = []
self.css_id = kwargs.get("css_id", self.css_id)
self.chart_options["chart"] = {}
if self.chart_type:
self.chart_options["chart"]["type"] = self.chart_type
[docs] def add_dataset(self, dataset: ApexDatasetBase) -> None:
"""
Add a dataset to the chart.
This method adds a dataset to the chart's series. Each dataset represents
a data series in the chart (e.g., a line in a line chart or a set of bars
in a bar chart).
Args:
dataset: The dataset to add to the chart
"""
self.chart_options["series"].append(dataset.get_options())
[docs] def add_categories(self, categories: list[Any]) -> None:
"""
Set the categories for the chart's x-axis.
This method sets the labels for the x-axis categories. These are typically
used for categorical data like months, product names, etc.
Args:
categories: List of category labels
"""
if "xaxis" not in self.chart_options:
self.chart_options["xaxis"] = {}
self.chart_options["xaxis"]["categories"] = categories
[docs] def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""
Get the context data for rendering the chart template.
This method prepares the data needed by the Django template to render
the chart, including chart options, CSS ID, and extra data.
Keyword Args:
**kwargs: Additional context data
Returns:
dict: The context data for the template
"""
kwargs["options"] = json.dumps(self.chart_options)
if not self.css_id:
self.css_id = str(random.randint(1, 100000)) # noqa: S311
kwargs["css_id"] = self.css_id
kwargs["wildewidgetclass"] = self.__class__.__name__
kwargs["extra_data"] = self.get_encoded_extra_data()
return kwargs
[docs] def add_suboption(self, option: str, name: str, value: Any) -> None:
"""
Add a nested configuration option to the chart.
This method adds a configuration option to a nested section of the chart
options. It's useful for setting options in sections like 'chart', 'xaxis',
'yaxis', etc.
Args:
option: The parent option section (e.g., 'chart', 'xaxis')
name: The name of the specific option
value: The value to set for the option
Example:
.. code-block:: python
# Set chart.height = 350
chart.add_suboption('chart', 'height', 350)
# Set tooltip.enabled = False
chart.add_suboption('tooltip', 'enabled', False)
"""
if option not in self.chart_options:
self.chart_options[option] = {}
self.chart_options[option][name] = value
def __str__(self) -> str:
"""
Return the HTML representation of the chart.
This method allows the chart to be used directly in Django templates.
Returns:
str: The rendered HTML for the chart
"""
return self.get_content()
[docs] def get_content(self, **kwargs: Any) -> str: # noqa: ARG002
"""
Render the chart as HTML content.
This method renders the chart using the specified template and context data.
Args:
**kwargs: Additional context data (unused)
Returns:
str: The rendered HTML content for the chart
"""
context = self.get_context_data()
html_template = template.loader.get_template(self.template_name)
return html_template.render(context)
[docs]class ApexSparkline(ApexChartBase):
"""
A specialized Apex chart for creating sparklines.
Sparklines are small, simple, condensed charts typically shown inline with
text or in a dashboard. This class configures an Apex chart with the
appropriate settings for a sparkline visualization.
Attributes:
width: The width of the sparkline chart in pixels
stroke_width: The width of the line/stroke in pixels
Example:
.. code-block:: python
sparkline = ApexSparkline()
dataset = MyDataset()
dataset.data = [10, 41, 35, 51, 49, 62, 69, 91, 148]
sparkline.add_dataset(dataset)
Or use subclassing to customize the sparkline further:
... code-block:: python
class CustomSparkline(ApexSparkline):
width = 100
stroke_width = 3
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_suboption("chart", "sparkline", {"enabled": True})
self.add_suboption("chart", "width", self.width)
self.add_suboption("stroke", "width", self.stroke_width)
self.add_dataset(self.get_dataset())
def get_dataset(self) -> list[Any]:
# do your custom data loading here
return (
Model.objects
.order_by('datestamp')
.values_list('field_name', flat=True)
)
"""
width: int = 65
stroke_width: int = 2
def __init__(self, **kwargs: Any) -> None:
"""
Initialize the sparkline chart with appropriate options.
Args:
**kwargs: Keyword arguments passed to the parent class
"""
super().__init__(**kwargs)
self.add_suboption("chart", "sparkline", {"enabled": True})
self.add_suboption("chart", "width", self.width)
self.add_suboption("stroke", "width", self.stroke_width)