Skip to content

Commit

Permalink
fix cache
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Jun 11, 2024
1 parent 19e4815 commit d63e654
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 59 deletions.
13 changes: 11 additions & 2 deletions ckanext/charts/assets/js/charts-select.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
/**
* CKAN Charts select using tom select
*/
ckan.module("charts-select", function ($, _) {
"use strict";

return {
initialize: function () {
$.proxyAll(this, /_/);

new TomSelect(this.el.find("select")[0], {
let selectEl = this.el.find("select")[0];

if (selectEl.tomselect) {
selectEl.tomselect.destroy();
}

new TomSelect(selectEl, {
plugins: {
'checkbox_options': {
'checkedClassNames': ['ts-checked'],
Expand All @@ -16,7 +25,7 @@ ckan.module("charts-select", function ($, _) {
'title': 'Remove all selected options',
}
},
maxItems: this.el.find("select").attr("maxItems") || null,
maxItems: selectEl.getAttribute("maxitems") || null,
});
}
};
Expand Down
15 changes: 9 additions & 6 deletions ckanext/charts/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import tempfile
import time
from abc import ABC, abstractmethod
from io import BytesIO

import pandas as pd
import pyarrow as pa
from pyarrow import orc

import ckan.plugins.toolkit as tk
Expand Down Expand Up @@ -48,16 +48,19 @@ def get_data(self, key: str) -> pd.DataFrame | None:
if not raw_data:
return None

return pa.deserialize_pandas(raw_data)
return pd.read_csv(BytesIO(raw_data)) # type: ignore

def set_data(self, key: str, data: pd.DataFrame):
"""Serialize data and save to redis"""
cache_ttl = config.get_redis_cache_ttl()

if cache_ttl:
self.client.setex(key, cache_ttl, pa.serialize_pandas(data).to_pybytes())
else:
self.client.set(key, value=pa.serialize_pandas(data).to_pybytes())
try:
if cache_ttl:
self.client.setex(key, cache_ttl, data.to_csv(index=False))
else:
self.client.set(key, value=data.to_csv(index=False))
except Exception as e:
log.exception("Failed to save data to Redis: %s", e)

def invalidate(self, key: str):
self.client.delete(key)
Expand Down
41 changes: 32 additions & 9 deletions ckanext/charts/chart_builders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def chart_title_field(self) -> dict[str, Any]:
"form_placeholder": "Chart title",
"group": "Styles",
"validators": [
self.get_validator("default")(""),
self.get_validator("default")(" "),
self.get_validator("unicode_safe"),
],
}
Expand Down Expand Up @@ -309,7 +309,10 @@ def y_axis_field(self, choices: list[dict[str, str]]) -> dict[str, Any]:
return field

def y_multi_axis_field(
self, choices: list[dict[str, str]], max_items: int = 0
self,
choices: list[dict[str, str]],
max_items: int = 0,
help_text: str = "Select one or more columns for the Y-axis",
) -> dict[str, Any]:
field = {
"field_name": "y",
Expand All @@ -319,21 +322,17 @@ def y_multi_axis_field(
"group": "Data",
"form_snippet": "chart_select.html",
"validators": [
self.get_validator("charts_if_empty_same_as")("values"),
self.get_validator("charts_if_empty_same_as")("x"),
self.get_validator("not_empty"),
self.get_validator("charts_to_list_if_string"),
self.get_validator("list_of_strings"),
],
"output_validators": [
self.get_validator("not_empty"),
self.get_validator("charts_to_list_if_string"),
self.get_validator("charts_list_to_csv"),
],
"output_validators": [self.get_validator("not_empty")],
"form_attrs": {
"multiple ": "1",
"class": "tom-select",
},
"help_text": "Select one or more columns for the Y-axis",
"help_text": help_text,
}

if max_items:
Expand All @@ -344,6 +343,30 @@ def y_multi_axis_field(

return field

def values_multi_field(
self,
choices: list[dict[str, str]],
max_items: int = 0,
help_text: str = "Select one or more values for the chart",
):
field = self.y_multi_axis_field(choices, max_items)

field.update(
{
"field_name": "values",
"label": "Values",
"validators": [
self.get_validator("charts_if_empty_same_as")("names"),
self.get_validator("not_empty"),
self.get_validator("charts_to_list_if_string"),
self.get_validator("list_of_strings"),
],
"help_text": help_text,
}
)

return field

def sort_x_field(self) -> dict[str, Any]:
return {
"field_name": "sort_x",
Expand Down
120 changes: 83 additions & 37 deletions ckanext/charts/chart_builders/chartjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def get_supported_forms(cls) -> list[type[Any]]:
ChartJSDoughnutForm,
ChartJSScatterForm,
ChartJSBubbleForm,
ChartJSRadarForm,
]


Expand Down Expand Up @@ -112,13 +113,6 @@ def to_json(self) -> str:

datasets = []

# TODO: hack, for view chart after creation
# for some reason, the validator didn't convert the y field to a list
if not isinstance(self.settings["y"], list):
self.settings["y"] = [
field.strip() for field in self.settings["y"].split(",")
]

for field in self.settings["y"]:
dataset = {"label": field, "data": self.df[field].tolist()}

Expand Down Expand Up @@ -167,7 +161,6 @@ def to_json(self) -> str:

for field in [self.settings["values"]]:
for label in data["data"]["labels"]:
# dataset_data.append(self.df[self.df[field] == label][field].size)
dataset_data.append(
self.convert_to_native_types(
self.df[self.df[self.settings["names"]] == label][field].sum()
Expand Down Expand Up @@ -268,6 +261,8 @@ def get_form_fields(self):


class ChartJSBubbleBuilder(ChartJSScatterBuilder):
min_bubble_radius = 5

def to_json(self) -> str:
data = {
"type": "bubble",
Expand All @@ -276,48 +271,44 @@ def to_json(self) -> str:
}

dataset_data = []

min_bubble_radius = 5
max_size = self.df[self.settings["size"]].max()

for _, data_series in self.df.iterrows():
for field in [self.settings["y"]]:
size_column: str = self.settings["size"]
max_size = self.df[size_column].max()

# Handle cases where max_size is zero or NaN values are present
# or the column is not numeric
try:
pd.to_numeric(max_size)
except ValueError:
raise ChartBuildError(f"Column '{size_column}' is not numeric")

if max_size == 0 or np.isnan(max_size):
bubble_radius = min_bubble_radius
else:
data_series_size = np.nan_to_num(data_series[size_column], nan=0)
bubble_radius = (data_series_size / max_size) * 30

if bubble_radius < min_bubble_radius:
bubble_radius = min_bubble_radius

dataset_data.append(
{
"x": data_series[self.settings["x"]],
"y": data_series[field],
# calculate the radius of the bubble
"r": bubble_radius,
"r": self._calculate_bubble_radius(data_series, max_size),
}
)

data["data"]["datasets"] = [
{
"label": self.settings["y"],
"data": dataset_data,
}
]
data["data"]["datasets"] = [{"label": self.settings["y"], "data": dataset_data}]

return json.dumps(data)

def _calculate_bubble_radius(self, data_series: pd.Series, max_size: int) -> int:
"""Calculate bubble radius based on the size column"""
size_column: str = self.settings["size"]

# Handle cases where max_size is zero or NaN values are present
# or the column is not numeric
try:
pd.to_numeric(max_size)
except ValueError:
raise ChartBuildError(f"Column '{size_column}' is not numeric")

if max_size == 0 or np.isnan(max_size):
bubble_radius = self.min_bubble_radius
else:
data_series_size = np.nan_to_num(data_series[size_column], nan=0)
bubble_radius = (data_series_size / max_size) * 30

if bubble_radius < self.min_bubble_radius:
bubble_radius = self.min_bubble_radius

return bubble_radius


class ChartJSBubbleForm(ChartJSScatterForm):
name = "Bubble"
Expand All @@ -331,3 +322,58 @@ def get_form_fields(self):
fields.append(self.size_field(columns))

return fields


class ChartJSRadarBuilder(ChartJsBuilder):
def to_json(self) -> str:
data = {
"type": "radar",
"data": {"labels": self.settings["values"]},
"options": self.settings,
}

datasets = []

for label in self.get_unique_values(self.df[self.settings["names"]]):
dataset_data = []

for value in self.settings["values"]:
try:
dataset_data.append(
self.df[self.df[self.settings["names"]] == label][value].item()
)
except ValueError:
# TODO: probably collision by name column, e.g two or more rows
# skip for now
continue

datasets.append({"label": label, "data": dataset_data})

data["data"]["datasets"] = datasets

return json.dumps(data)


class ChartJSRadarForm(BaseChartForm):
name = "Radar"
builder = ChartJSRadarBuilder

def get_form_fields(self):
columns = [{"value": col, "label": col} for col in self.df.columns]
chart_types = [
{"value": form.name, "label": form.name}
for form in self.builder.get_supported_forms()
]

return [
self.title_field(),
self.description_field(),
self.engine_field(),
self.type_field(chart_types),
self.names_field(columns),
self.values_multi_field(
columns,
help_text="Select 3 or more different categorical variables (dimensions)",
),
self.limit_field(),
]
3 changes: 2 additions & 1 deletion ckanext/charts/chart_builders/plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ def build_line_chart(self) -> Any:
secondary_y=True,
)

fig.update_layout(title_text=self.settings["chart_title"])
if chart_title := self.settings.get("chart_title"):
fig.update_layout(title_text=chart_title)

return fig.to_json()

Expand Down
4 changes: 2 additions & 2 deletions ckanext/charts/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def setup_template_variables(
settings, _ = tk.navl_validate(
data_dict["resource_view"], settings_schema(), context
)
except exception.ChartTypeNotImplementedError:
except Exception as e:
data["error_msg"] = e
return data

# view create or edit
Expand Down Expand Up @@ -177,7 +178,6 @@ def after_upload(
dataset_dict: dict[str, Any],
) -> None:
"""Invalidate cache after upload to DataStore"""
import ipdb; ipdb.set_trace()
cache.invalidate_by_key(
fetchers.DatastoreDataFetcher(resource_dict["id"]).make_cache_key(),
)
Expand Down
13 changes: 13 additions & 0 deletions ckanext/charts/templates/package/edit_view.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
{% ckan_extends %}

{% block wrapper_class %} {% if resource_view.view_type == 'charts_view' %}charts-view{% endif %} {{ super() }}{% endblock %}

{% block form %}
<form class="dataset-form dataset-resource-form" method="post" data-module="basic-form resource-form">
{{ h.csrf_input() }}
{% include 'package/snippets/view_form.html' %}

{# removing preview button #}
<div class="form-actions">
<button class="btn btn-danger pull-left" name="delete" value="Delete"> {{ _('Delete') }} </button>
<button class="btn btn-primary" name="save" value="Save" type="submit">{{ _('Update') }}</button>
</div>
</form>
{% endblock %}
Loading

0 comments on commit d63e654

Please sign in to comment.