diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index bfe18330fe..924ab93507 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,132 +1,3 @@ # Contributor Covenant Code of Conduct -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -vegajs.conduct@gmail.com. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations +As a project of the Vega Organization, we use the [Vega Code of Conduct](https://github.com/vega/.github/blob/main/CODE_OF_CONDUCT.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9b0920ffa..37fb39bcea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ visualization. We are also seeking contributions of additional Jupyter notebook-based examples in our separate GitHub repository: https://github.com/altair-viz/altair_notebooks. -All contributions, suggestions, and feedback you submitted are accepted under the [Project's license](./LICENSE). You represent that if you do not own copyright in the code that you have the authority to submit it under the [Project's license](./LICENSE). All feedback, suggestions, or contributions are not confidential. +All contributions, suggestions, and feedback you submitted are accepted under the [Project's license](./LICENSE). You represent that if you do not own copyright in the code that you have the authority to submit it under the [Project's license](./LICENSE). All feedback, suggestions, or contributions are not confidential. The Project abides by the Vega Organization's [code of conduct](https://github.com/vega/.github/blob/main/CODE_OF_CONDUCT.md) and [governance](https://github.com/vega/.github/blob/main/project-docs/GOVERNANCE.md). ## How To Contribute Code to Vega-Altair @@ -203,7 +203,6 @@ hatch run doc:serve To view the documentation, open your browser and go to `http://localhost:8000`. To stop the server, use `^C` (control+c) in the terminal. --- -The Project abides by the Organization's [code of conduct](./governance/org-docs/CODE-OF-CONDUCT.md) and [trademark policy](./governance/org-docs/TRADEMARKS.md). Part of MVG-0.1-beta. Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/NOTES_FOR_MAINTAINERS.md b/NOTES_FOR_MAINTAINERS.md index a91a3a2f20..66b1c7ce21 100644 --- a/NOTES_FOR_MAINTAINERS.md +++ b/NOTES_FOR_MAINTAINERS.md @@ -1,6 +1,5 @@ # Notes for Maintainers of Altair - ## Auto-generating the Python code The core Python API for Altair can be found in the following locations: @@ -61,6 +60,7 @@ import embed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.15.1 ``` ### Updating vl-convert version bound + When updating the version of Vega-Lite, it's important to ensure that [vl-convert](https://github.com/vega/vl-convert) includes support for the new Vega-Lite version. Check the [vl-convert releases](https://github.com/vega/vl-convert/releases) to find the minimum @@ -72,4 +72,4 @@ with the new minimum required version of vl-convert. ## Releasing the Package To cut a new release of Altair, follow the steps outlined in -[RELEASING.md](RELEASING.md). \ No newline at end of file +[RELEASING.md](RELEASING.md). diff --git a/README.md b/README.md index 23fea01255..e162e22e09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Vega-Altair - [![github actions](https://github.com/altair-viz/altair/workflows/build/badge.svg)](https://github.com/altair-viz/altair/actions?query=workflow%3Abuild) [![code style black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![JOSS Paper](https://joss.theoj.org/papers/10.21105/joss.01057/status.svg)](https://joss.theoj.org/papers/10.21105/joss.01057) @@ -77,6 +76,7 @@ points & bars ![Vega-Altair Visualization Gif](https://raw.githubusercontent.com/altair-viz/altair/main/images/cars_scatter_bar.gif) ## Features + * Carefully-designed, declarative Python API. * Auto-generated internal Python API that guarantees visualizations are type-checked and in full conformance with the [Vega-Lite](https://github.com/vega/vega-lite) @@ -88,6 +88,7 @@ points & bars * Serialize visualizations as JSON files. ## Installation + Vega-Altair can be installed with: ```bash pip install altair @@ -101,14 +102,17 @@ conda install altair -c conda-forge For full installation instructions, please see [the documentation](https://altair-viz.github.io/getting_started/installation.html). ## Getting Help + If you have a question that is not addressed in the documentation, you can post it on [StackOverflow](https://stackoverflow.com/questions/tagged/altair) using the `altair` tag. For bugs and feature requests, please open a [Github Issue](https://github.com/altair-viz/altair/issues). ## Development + You can find the instructions on how to install the package for development in [the documentation](https://altair-viz.github.io/getting_started/installation.html). To run the tests and linters, use + ``` hatch run test ``` @@ -117,6 +121,7 @@ For information on how to contribute your developments back to the Vega-Altair r [`CONTRIBUTING.md`](https://github.com/altair-viz/altair/blob/main/CONTRIBUTING.md) ## Citing Vega-Altair + [![JOSS Paper](https://joss.theoj.org/papers/10.21105/joss.01057/status.svg)](https://joss.theoj.org/papers/10.21105/joss.01057) If you use Vega-Altair in academic work, please consider citing https://joss.theoj.org/papers/10.21105/joss.01057 as @@ -136,6 +141,7 @@ If you use Vega-Altair in academic work, please consider citing https://joss.the } ``` Please additionally consider citing the [Vega-Lite](https://vega.github.io/vega-lite/) project, which Vega-Altair is based on: https://dl.acm.org/doi/10.1109/TVCG.2016.2599030 + ```bib @article{Satyanarayan2017, author={Satyanarayan, Arvind and Moritz, Dominik and Wongsuphasawat, Kanit and Heer, Jeffrey}, diff --git a/altair/jupyter/js/index.js b/altair/jupyter/js/index.js index 81ad634e66..9202e99be6 100644 --- a/altair/jupyter/js/index.js +++ b/altair/jupyter/js/index.js @@ -1,5 +1,5 @@ -import embed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.16.3"; -import debounce from "https://esm.sh/lodash-es@4.17.21/debounce"; +import vegaEmbed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.16.3"; +import lodashDebounce from "https://esm.sh/lodash-es@4.17.21/debounce"; export async function render({ model, el }) { let finalize; @@ -19,10 +19,21 @@ export async function render({ model, el }) { finalize(); } - let spec = model.get("spec"); + model.set("local_tz", Intl.DateTimeFormat().resolvedOptions().timeZone); + + let spec = structuredClone(model.get("spec")); + if (spec == null) { + // Remove any existing chart and return + while (el.firstChild) { + el.removeChild(el.lastChild); + } + model.save_changes(); + return; + } + let api; try { - api = await embed(el, spec); + api = await vegaEmbed(el, spec); } catch (error) { showError(error) return; @@ -32,7 +43,10 @@ export async function render({ model, el }) { // Debounce config const wait = model.get("debounce_wait") ?? 10; - const maxWait = wait; + const debounceOpts = {leading: false, trailing: true}; + if (model.get("max_wait") ?? true) { + debounceOpts["maxWait"] = wait; + } const initialSelections = {}; for (const selectionName of Object.keys(model.get("_vl_selections"))) { @@ -45,7 +59,7 @@ export async function render({ model, el }) { model.set("_vl_selections", newSelections); model.save_changes(); }; - api.view.addSignalListener(selectionName, debounce(selectionHandler, wait, {maxWait})); + api.view.addSignalListener(selectionName, lodashDebounce(selectionHandler, wait, debounceOpts)); initialSelections[selectionName] = { value: cleanJson(api.view.signal(selectionName) ?? {}), @@ -62,7 +76,7 @@ export async function render({ model, el }) { model.set("_params", newParams); model.save_changes(); }; - api.view.addSignalListener(paramName, debounce(paramHandler, wait, {maxWait})); + api.view.addSignalListener(paramName, lodashDebounce(paramHandler, wait, debounceOpts)); initialParams[paramName] = api.view.signal(paramName) ?? null } @@ -76,13 +90,132 @@ export async function render({ model, el }) { } await api.view.runAsync(); }); + + // Add signal/data listeners + for (const watch of model.get("_js_watch_plan") ?? []) { + if (watch.namespace === "data") { + const dataHandler = (_, value) => { + model.set("_js_to_py_updates", [{ + namespace: "data", + name: watch.name, + scope: watch.scope, + value: cleanJson(value) + }]); + model.save_changes(); + }; + addDataListener(api.view, watch.name, watch.scope, lodashDebounce(dataHandler, wait, debounceOpts)) + + } else if (watch.namespace === "signal") { + const signalHandler = (_, value) => { + model.set("_js_to_py_updates", [{ + namespace: "signal", + name: watch.name, + scope: watch.scope, + value: cleanJson(value) + }]); + model.save_changes(); + }; + + addSignalListener(api.view, watch.name, watch.scope, lodashDebounce(signalHandler, wait, debounceOpts)) + } + } + + // Add signal/data updaters + model.on('change:_py_to_js_updates', async (updates) => { + for (const update of updates.changed._py_to_js_updates ?? []) { + if (update.namespace === "signal") { + setSignalValue(api.view, update.name, update.scope, update.value); + } else if (update.namespace === "data") { + setDataValue(api.view, update.name, update.scope, update.value); + } + } + await api.view.runAsync(); + }); } model.on('change:spec', reembed); model.on('change:debounce_wait', reembed); + model.on('change:max_wait', reembed); await reembed(); } function cleanJson(data) { return JSON.parse(JSON.stringify(data)) +} + +function getNestedRuntime(view, scope) { + var runtime = view._runtime; + for (const index of scope) { + runtime = runtime.subcontext[index]; + } + return runtime +} + +function lookupSignalOp(view, name, scope) { + let parent_runtime = getNestedRuntime(view, scope); + return parent_runtime.signals[name] ?? null; +} + +function dataRef(view, name, scope) { + let parent_runtime = getNestedRuntime(view, scope); + return parent_runtime.data[name]; +} + +export function setSignalValue(view, name, scope, value) { + let signal_op = lookupSignalOp(view, name, scope); + view.update(signal_op, value); +} + +export function setDataValue(view, name, scope, value) { + let dataset = dataRef(view, name, scope); + let changeset = view.changeset().remove(() => true).insert(value) + dataset.modified = true; + view.pulse(dataset.input, changeset); +} + +export function addSignalListener(view, name, scope, handler) { + let signal_op = lookupSignalOp(view, name, scope); + return addOperatorListener( + view, + name, + signal_op, + handler, + ); +} + +export function addDataListener(view, name, scope, handler) { + let dataset = dataRef(view, name, scope).values; + return addOperatorListener( + view, + name, + dataset, + handler, + ); +} + +// Private helpers from Vega for dealing with nested signals/data +function findOperatorHandler(op, handler) { + const h = (op._targets || []) + .filter(op => op._update && op._update.handler === handler); + return h.length ? h[0] : null; +} + +function addOperatorListener(view, name, op, handler) { + let h = findOperatorHandler(op, handler); + if (!h) { + h = trap(view, () => handler(name, op.value)); + h.handler = handler; + view.on(op, null, h); + } + return view; +} + +function trap(view, fn) { + return !fn ? null : function() { + try { + fn.apply(this, arguments); + } catch (error) { + view.error(error); + } + }; } \ No newline at end of file diff --git a/altair/jupyter/jupyter_chart.py b/altair/jupyter/jupyter_chart.py index 5b2f4af686..fc973a5a84 100644 --- a/altair/jupyter/jupyter_chart.py +++ b/altair/jupyter/jupyter_chart.py @@ -1,10 +1,14 @@ +import json import anywidget import traitlets import pathlib from typing import Any, Set import altair as alt -from altair.utils._vegafusion_data import using_vegafusion +from altair.utils._vegafusion_data import ( + using_vegafusion, + compile_to_vegafusion_chart_state, +) from altair import TopLevelSpec from altair.utils.selection import IndexSelection, PointSelection, IntervalSelection @@ -20,9 +24,7 @@ def __init__(self, trait_values): super().__init__() for key, value in trait_values.items(): - if isinstance(value, int): - traitlet_type = traitlets.Int() - elif isinstance(value, float): + if isinstance(value, (int, float)): traitlet_type = traitlets.Float() elif isinstance(value, str): traitlet_type = traitlets.Unicode() @@ -101,9 +103,12 @@ class JupyterChart(anywidget.AnyWidget): """ # Public traitlets - chart = traitlets.Instance(TopLevelSpec) - spec = traitlets.Dict().tag(sync=True) + chart = traitlets.Instance(TopLevelSpec, allow_none=True) + spec = traitlets.Dict(allow_none=True).tag(sync=True) debounce_wait = traitlets.Float(default_value=10).tag(sync=True) + max_wait = traitlets.Bool(default_value=True).tag(sync=True) + local_tz = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True) + debug = traitlets.Bool(default_value=False) # Internal selection traitlets _selection_types = traitlets.Dict() @@ -112,7 +117,20 @@ class JupyterChart(anywidget.AnyWidget): # Internal param traitlets _params = traitlets.Dict().tag(sync=True) - def __init__(self, chart: TopLevelSpec, debounce_wait: int = 10, **kwargs: Any): + # Internal comm traitlets for VegaFusion support + _chart_state = traitlets.Any(allow_none=True) + _js_watch_plan = traitlets.Any(allow_none=True).tag(sync=True) + _js_to_py_updates = traitlets.Any(allow_none=True).tag(sync=True) + _py_to_js_updates = traitlets.Any(allow_none=True).tag(sync=True) + + def __init__( + self, + chart: TopLevelSpec, + debounce_wait: int = 10, + max_wait: bool = True, + debug: bool = False, + **kwargs: Any, + ): """ Jupyter Widget for displaying and updating Altair Charts, and retrieving selection and parameter values @@ -122,11 +140,24 @@ def __init__(self, chart: TopLevelSpec, debounce_wait: int = 10, **kwargs: Any): chart: Chart Altair Chart instance debounce_wait: int - Debouncing wait time in milliseconds + Debouncing wait time in milliseconds. Updates will be sent from the client to the kernel + after debounce_wait milliseconds of no chart interactions. + max_wait: bool + If True (default), updates will be sent from the client to the kernel every debounce_wait + milliseconds even if there are ongoing chart interactions. If False, updates will not be + sent until chart interactions have completed. + debug: bool + If True, debug messages will be printed """ self.params = Params({}) self.selections = Selections({}) - super().__init__(chart=chart, debounce_wait=debounce_wait, **kwargs) + super().__init__( + chart=chart, + debounce_wait=debounce_wait, + max_wait=max_wait, + debug=debug, + **kwargs, + ) @traitlets.observe("chart") def _on_change_chart(self, change): @@ -135,14 +166,22 @@ def _on_change_chart(self, change): state when the wrapped Chart instance changes """ new_chart = change.new - - params = getattr(new_chart, "params", []) selection_watches = [] selection_types = {} initial_params = {} initial_vl_selections = {} empty_selections = {} + if new_chart is None: + with self.hold_sync(): + self.spec = None + self._selection_types = selection_types + self._vl_selections = initial_vl_selections + self._params = initial_params + return + + params = getattr(new_chart, "params", []) + if params is not alt.Undefined: for param in new_chart.params: if isinstance(param.name, alt.ParameterName): @@ -205,13 +244,50 @@ def on_param_traitlet_changed(param_change): # Update properties all together with self.hold_sync(): if using_vegafusion(): - self.spec = new_chart.to_dict(format="vega") + if self.local_tz is None: + self.spec = None + + def on_local_tz_change(change): + self._init_with_vegafusion(change["new"]) + + self.observe(on_local_tz_change, ["local_tz"]) + else: + self._init_with_vegafusion(self.local_tz) else: self.spec = new_chart.to_dict() self._selection_types = selection_types self._vl_selections = initial_vl_selections self._params = initial_params + def _init_with_vegafusion(self, local_tz: str): + if self.chart is not None: + vegalite_spec = self.chart.to_dict(context={"pre_transform": False}) + with self.hold_sync(): + self._chart_state = compile_to_vegafusion_chart_state( + vegalite_spec, local_tz + ) + self._js_watch_plan = self._chart_state.get_watch_plan()[ + "client_to_server" + ] + self.spec = self._chart_state.get_transformed_spec() + + # Callback to update chart state and send updates back to client + def on_js_to_py_updates(change): + if self.debug: + updates_str = json.dumps(change["new"], indent=2) + print( + f"JavaScript to Python VegaFusion updates:\n {updates_str}" + ) + updates = self._chart_state.update(change["new"]) + if self.debug: + updates_str = json.dumps(updates, indent=2) + print( + f"Python to JavaScript VegaFusion updates:\n {updates_str}" + ) + self._py_to_js_updates = updates + + self.observe(on_js_to_py_updates, ["_js_to_py_updates"]) + @traitlets.observe("_params") def _on_change_params(self, change): for param_name, value in change.new.items(): diff --git a/altair/utils/_dfi_types.py b/altair/utils/_dfi_types.py index 16b83fb4df..a76435e7fd 100644 --- a/altair/utils/_dfi_types.py +++ b/altair/utils/_dfi_types.py @@ -1,34 +1,11 @@ # DataFrame Interchange Protocol Types -# Copied from https://data-apis.org/dataframe-protocol/latest/API.html +# Copied from https://data-apis.org/dataframe-protocol/latest/API.html, +# changed ABCs to Protocols, and subset the type hints to only those that are +# relevant for Altair. # # These classes are only for use in type signatures -from abc import ( - ABC, - abstractmethod, -) import enum -from typing import ( - Any, - Dict, - Iterable, - Optional, - Sequence, - Tuple, - TypedDict, -) - - -class DlpackDeviceType(enum.IntEnum): - """Integer enum for device type codes matching DLPack.""" - - CPU = 1 - CUDA = 2 - CPU_PINNED = 3 - OPENCL = 4 - VULKAN = 7 - METAL = 8 - VPI = 9 - ROCM = 10 +from typing import Any, Iterable, Optional, Tuple, Protocol class DtypeKind(enum.IntEnum): @@ -62,188 +39,15 @@ class DtypeKind(enum.IntEnum): CATEGORICAL = 23 -Dtype = Tuple[DtypeKind, int, str, str] # see Column.dtype - - -class ColumnNullType(enum.IntEnum): - """ - Integer enum for null type representation. - - Attributes - ---------- - NON_NULLABLE : int - Non-nullable column. - USE_NAN : int - Use explicit float NaN value. - USE_SENTINEL : int - Sentinel value besides NaN. - USE_BITMASK : int - The bit is set/unset representing a null on a certain position. - USE_BYTEMASK : int - The byte is set/unset representing a null on a certain position. - """ - - NON_NULLABLE = 0 - USE_NAN = 1 - USE_SENTINEL = 2 - USE_BITMASK = 3 - USE_BYTEMASK = 4 - - -class ColumnBuffers(TypedDict): - # first element is a buffer containing the column data; - # second element is the data buffer's associated dtype - data: Tuple["Buffer", Dtype] - - # first element is a buffer containing mask values indicating missing data; - # second element is the mask value buffer's associated dtype. - # None if the null representation is not a bit or byte mask - validity: Optional[Tuple["Buffer", Dtype]] - - # first element is a buffer containing the offset values for - # variable-size binary data (e.g., variable-length strings); - # second element is the offsets buffer's associated dtype. - # None if the data buffer does not have an associated offsets buffer - offsets: Optional[Tuple["Buffer", Dtype]] - - -class CategoricalDescription(TypedDict): - # whether the ordering of dictionary indices is semantically meaningful - is_ordered: bool - # whether a dictionary-style mapping of categorical values to other objects exists - is_dictionary: bool - # Python-level only (e.g. ``{int: str}``). - # None if not a dictionary-style categorical. - categories: "Optional[Column]" +# Type hint of first element would actually be DtypeKind but can't use that +# as other libraries won't use an instance of our own Enum in this module but have +# their own. Type checkers will raise an error on that even though the enums +# are identical. +Dtype = Tuple[Any, int, str, str] # see Column.dtype -class Buffer(ABC): - """ - Data in the buffer is guaranteed to be contiguous in memory. - - Note that there is no dtype attribute present, a buffer can be thought of - as simply a block of memory. However, if the column that the buffer is - attached to has a dtype that's supported by DLPack and ``__dlpack__`` is - implemented, then that dtype information will be contained in the return - value from ``__dlpack__``. - - This distinction is useful to support both data exchange via DLPack on a - buffer and (b) dtypes like variable-length strings which do not have a - fixed number of bytes per element. - """ - +class Column(Protocol): @property - @abstractmethod - def bufsize(self) -> int: - """ - Buffer size in bytes. - """ - pass - - @property - @abstractmethod - def ptr(self) -> int: - """ - Pointer to start of the buffer as an integer. - """ - pass - - @abstractmethod - def __dlpack__(self): - """ - Produce DLPack capsule (see array API standard). - - Raises: - - - TypeError : if the buffer contains unsupported dtypes. - - NotImplementedError : if DLPack support is not implemented - - Useful to have to connect to array libraries. Support optional because - it's not completely trivial to implement for a Python-only library. - """ - raise NotImplementedError("__dlpack__") - - @abstractmethod - def __dlpack_device__(self) -> Tuple[DlpackDeviceType, Optional[int]]: - """ - Device type and device ID for where the data in the buffer resides. - Uses device type codes matching DLPack. - Note: must be implemented even if ``__dlpack__`` is not. - """ - pass - - -class Column(ABC): - """ - A column object, with only the methods and properties required by the - interchange protocol defined. - - A column can contain one or more chunks. Each chunk can contain up to three - buffers - a data buffer, a mask buffer (depending on null representation), - and an offsets buffer (if variable-size binary; e.g., variable-length - strings). - - TBD: Arrow has a separate "null" dtype, and has no separate mask concept. - Instead, it seems to use "children" for both columns with a bit mask, - and for nested dtypes. Unclear whether this is elegant or confusing. - This design requires checking the null representation explicitly. - - The Arrow design requires checking: - 1. the ARROW_FLAG_NULLABLE (for sentinel values) - 2. if a column has two children, combined with one of those children - having a null dtype. - - Making the mask concept explicit seems useful. One null dtype would - not be enough to cover both bit and byte masks, so that would mean - even more checking if we did it the Arrow way. - - TBD: there's also the "chunk" concept here, which is implicit in Arrow as - multiple buffers per array (= column here). Semantically it may make - sense to have both: chunks were meant for example for lazy evaluation - of data which doesn't fit in memory, while multiple buffers per column - could also come from doing a selection operation on a single - contiguous buffer. - - Given these concepts, one would expect chunks to be all of the same - size (say a 10,000 row dataframe could have 10 chunks of 1,000 rows), - while multiple buffers could have data-dependent lengths. Not an issue - in pandas if one column is backed by a single NumPy array, but in - Arrow it seems possible. - Are multiple chunks *and* multiple buffers per column necessary for - the purposes of this interchange protocol, or must producers either - reuse the chunk concept for this or copy the data? - - Note: this Column object can only be produced by ``__dataframe__``, so - doesn't need its own version or ``__column__`` protocol. - """ - - @abstractmethod - def size(self) -> int: - """ - Size of the column, in elements. - - Corresponds to DataFrame.num_rows() if column is a single chunk; - equal to size of this current chunk otherwise. - - Is a method rather than a property because it may cause a (potentially - expensive) computation for some dataframe implementations. - """ - pass - - @property - @abstractmethod - def offset(self) -> int: - """ - Offset of first element. - - May be > 0 if using chunks; for example for a column with N chunks of - equal size M (only the last chunk may be shorter), - ``offset = n * M``, ``n = 0 .. N-1``. - """ - pass - - @property - @abstractmethod def dtype(self) -> Dtype: """ Dtype description as a tuple ``(kind, bit-width, format string, endianness)``. @@ -274,9 +78,13 @@ def dtype(self) -> Dtype: """ pass + # Have to use a generic Any return type as not all libraries who implement + # the dataframe interchange protocol implement the TypedDict that is usually + # returned here in the same way. As TypedDicts are invariant, even a slight change + # will lead to an error by a type checker. See PR in which this code was added + # for details. @property - @abstractmethod - def describe_categorical(self) -> CategoricalDescription: + def describe_categorical(self) -> Any: """ If the dtype is categorical, there are two options: - There are only values in the data buffer. @@ -297,87 +105,8 @@ def describe_categorical(self) -> CategoricalDescription: """ pass - @property - @abstractmethod - def describe_null(self) -> Tuple[ColumnNullType, Any]: - """ - Return the missing value (or "null") representation the column dtype - uses, as a tuple ``(kind, value)``. - - Value : if kind is "sentinel value", the actual value. If kind is a bit - mask or a byte mask, the value (0 or 1) indicating a missing value. None - otherwise. - """ - pass - - @property - @abstractmethod - def null_count(self) -> Optional[int]: - """ - Number of null elements, if known. - - Note: Arrow uses -1 to indicate "unknown", but None seems cleaner. - """ - pass - - @property - @abstractmethod - def metadata(self) -> Dict[str, Any]: - """ - The metadata for the column. See `DataFrame.metadata` for more details. - """ - pass - - @abstractmethod - def num_chunks(self) -> int: - """ - Return the number of chunks the column consists of. - """ - pass - - @abstractmethod - def get_chunks(self, n_chunks: Optional[int] = None) -> Iterable["Column"]: - """ - Return an iterator yielding the chunks. - - See `DataFrame.get_chunks` for details on ``n_chunks``. - """ - pass - - @abstractmethod - def get_buffers(self) -> ColumnBuffers: - """ - Return a dictionary containing the underlying buffers. - The returned dictionary has the following contents: - - - "data": a two-element tuple whose first element is a buffer - containing the data and whose second element is the data - buffer's associated dtype. - - "validity": a two-element tuple whose first element is a buffer - containing mask values indicating missing data and - whose second element is the mask value buffer's - associated dtype. None if the null representation is - not a bit or byte mask. - - "offsets": a two-element tuple whose first element is a buffer - containing the offset values for variable-size binary - data (e.g., variable-length strings) and whose second - element is the offsets buffer's associated dtype. None - if the data buffer does not have an associated offsets - buffer. - """ - pass - - -# def get_children(self) -> Iterable[Column]: -# """ -# Children columns underneath the column, each object in this iterator -# must adhere to the column specification. -# """ -# pass - - -class DataFrame(ABC): +class DataFrame(Protocol): """ A data frame class, with only the methods required by the interchange protocol defined. @@ -392,9 +121,6 @@ class DataFrame(ABC): to the dataframe interchange protocol specification. """ - version = 0 # version of the protocol - - @abstractmethod def __dataframe__( self, nan_as_null: bool = False, allow_copy: bool = True ) -> "DataFrame": @@ -412,87 +138,18 @@ def __dataframe__( """ pass - @property - @abstractmethod - def metadata(self) -> Dict[str, Any]: - """ - The metadata for the data frame, as a dictionary with string keys. The - contents of `metadata` may be anything, they are meant for a library - to store information that it needs to, e.g., roundtrip losslessly or - for two implementations to share data that is not (yet) part of the - interchange protocol specification. For avoiding collisions with other - entries, please add name the keys with the name of the library - followed by a period and the desired name, e.g, ``pandas.indexcol``. - """ - pass - - @abstractmethod - def num_columns(self) -> int: - """ - Return the number of columns in the DataFrame. - """ - pass - - @abstractmethod - def num_rows(self) -> Optional[int]: - # TODO: not happy with Optional, but need to flag it may be expensive - # why include it if it may be None - what do we expect consumers - # to do here? - """ - Return the number of rows in the DataFrame, if available. - """ - pass - - @abstractmethod - def num_chunks(self) -> int: - """ - Return the number of chunks the DataFrame consists of. - """ - pass - - @abstractmethod def column_names(self) -> Iterable[str]: """ Return an iterator yielding the column names. """ pass - @abstractmethod - def get_column(self, i: int) -> Column: - """ - Return the column at the indicated position. - """ - pass - - @abstractmethod def get_column_by_name(self, name: str) -> Column: """ Return the column whose name is the indicated name. """ pass - @abstractmethod - def get_columns(self) -> Iterable[Column]: - """ - Return an iterator yielding the columns. - """ - pass - - @abstractmethod - def select_columns(self, indices: Sequence[int]) -> "DataFrame": - """ - Create a new DataFrame by selecting a subset of columns by index. - """ - pass - - @abstractmethod - def select_columns_by_name(self, names: Sequence[str]) -> "DataFrame": - """ - Create a new DataFrame by selecting a subset of columns by name. - """ - pass - - @abstractmethod def get_chunks(self, n_chunks: Optional[int] = None) -> Iterable["DataFrame"]: """ Return an iterator yielding the chunks. diff --git a/altair/utils/_importers.py b/altair/utils/_importers.py index c8cd21c950..130d2a679e 100644 --- a/altair/utils/_importers.py +++ b/altair/utils/_importers.py @@ -4,13 +4,17 @@ def import_vegafusion() -> ModuleType: - min_version = "1.4.0" + min_version = "1.5.0" try: version = importlib_version("vegafusion") - if Version(version) < Version(min_version): + embed_version = importlib_version("vegafusion-python-embed") + if version != embed_version or Version(version) < Version(min_version): raise RuntimeError( - f"The vegafusion package must be version {min_version} or greater. " - f"Found version {version}" + "The versions of the vegafusion and vegafusion-python-embed packages must match\n" + f"and must be version {min_version} or greater.\n" + f"Found:\n" + f" - vegafusion=={version}\n" + f" - vegafusion-python-embed=={embed_version}\n" ) import vegafusion as vf # type: ignore diff --git a/altair/utils/_vegafusion_data.py b/altair/utils/_vegafusion_data.py index 65585e5bf6..8b46bab780 100644 --- a/altair/utils/_vegafusion_data.py +++ b/altair/utils/_vegafusion_data.py @@ -2,15 +2,24 @@ import uuid from weakref import WeakValueDictionary -from typing import Union, Dict, Set, MutableMapping - -from typing import TypedDict, Final +from typing import ( + Union, + Dict, + Set, + MutableMapping, + TypedDict, + Final, + TYPE_CHECKING, +) from altair.utils._importers import import_vegafusion from altair.utils.core import DataFrameLike from altair.utils.data import DataType, ToValuesReturnType, MaxRowsError from altair.vegalite.data import default_data_transformer +if TYPE_CHECKING: + from vegafusion.runtime import ChartState # type: ignore + # Temporary storage for dataframes that have been extracted # from charts by the vegafusion data transformer. Use a WeakValueDictionary # rather than a dict so that the Python interpreter is free to garbage @@ -124,6 +133,60 @@ def get_inline_tables(vega_spec: dict) -> Dict[str, DataFrameLike]: return tables +def compile_to_vegafusion_chart_state( + vegalite_spec: dict, local_tz: str +) -> "ChartState": + """Compile a Vega-Lite spec to a VegaFusion ChartState + + Note: This function should only be called on a Vega-Lite spec + that was generated with the "vegafusion" data transformer enabled. + In particular, this spec may contain references to extract datasets + using table:// prefixed URLs. + + Parameters + ---------- + vegalite_spec: dict + A Vega-Lite spec that was generated from an Altair chart with + the "vegafusion" data transformer enabled + local_tz: str + Local timezone name (e.g. 'America/New_York') + + Returns + ------- + ChartState + A VegaFusion ChartState object + """ + # Local import to avoid circular ImportError + from altair import vegalite_compilers, data_transformers + + vf = import_vegafusion() + + # Compile Vega-Lite spec to Vega + compiler = vegalite_compilers.get() + if compiler is None: + raise ValueError("No active vega-lite compiler plugin found") + + vega_spec = compiler(vegalite_spec) + + # Retrieve dict of inline tables referenced by the spec + inline_tables = get_inline_tables(vega_spec) + + # Pre-evaluate transforms in vega spec with vegafusion + row_limit = data_transformers.options.get("max_rows", None) + + chart_state = vf.runtime.new_chart_state( + vega_spec, + local_tz=local_tz, + inline_datasets=inline_tables, + row_limit=row_limit, + ) + + # Check from row limit warning and convert to MaxRowsError + handle_row_limit_exceeded(row_limit, chart_state.get_warnings()) + + return chart_state + + def compile_with_vegafusion(vegalite_spec: dict) -> dict: """Compile a Vega-Lite spec to Vega and pre-transform with VegaFusion @@ -168,6 +231,12 @@ def compile_with_vegafusion(vegalite_spec: dict) -> dict: ) # Check from row limit warning and convert to MaxRowsError + handle_row_limit_exceeded(row_limit, warnings) + + return transformed_vega_spec + + +def handle_row_limit_exceeded(row_limit: int, warnings: list): for warning in warnings: if warning.get("type") == "RowLimitExceeded": raise MaxRowsError( @@ -178,8 +247,6 @@ def compile_with_vegafusion(vegalite_spec: dict) -> dict: "disabling this limit may cause the browser to freeze or crash." ) - return transformed_vega_spec - def using_vegafusion() -> bool: """Check whether the vegafusion data transformer is enabled""" diff --git a/altair/utils/core.py b/altair/utils/core.py index ea8abf1f18..f6fec38016 100644 --- a/altair/utils/core.py +++ b/altair/utils/core.py @@ -46,7 +46,9 @@ class DataFrameLike(Protocol): - def __dataframe__(self, *args, **kwargs) -> DfiDataFrame: + def __dataframe__( + self, nan_as_null: bool = False, allow_copy: bool = True + ) -> DfiDataFrame: ... diff --git a/altair/utils/data.py b/altair/utils/data.py index 2a3a3474bf..7fba7adaa1 100644 --- a/altair/utils/data.py +++ b/altair/utils/data.py @@ -257,7 +257,7 @@ def check_data_type(data: DataType) -> None: # Private utilities # ============================================================================== def _compute_data_hash(data_str: str) -> str: - return hashlib.md5(data_str.encode()).hexdigest() + return hashlib.sha256(data_str.encode()).hexdigest()[:32] def _data_to_json_string(data: DataType) -> str: diff --git a/altair/utils/mimebundle.py b/altair/utils/mimebundle.py index d484700418..a06f1f199b 100644 --- a/altair/utils/mimebundle.py +++ b/altair/utils/mimebundle.py @@ -72,9 +72,11 @@ def spec_to_mimebundle( # Default to the embed options set by alt.renderers.set_embed_options if embed_options is None: - embed_options = renderers.options.get("embed_options", {}) + final_embed_options = renderers.options.get("embed_options", {}) + else: + final_embed_options = embed_options - embed_options = preprocess_embed_options(embed_options) + embed_options = preprocess_embed_options(final_embed_options) if format in ["png", "svg", "pdf", "vega"]: format = cast(Literal["png", "svg", "pdf", "vega"], format) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index c87c2e85d5..56b15db79b 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -57,7 +57,7 @@ def _dataset_name(values: Union[dict, list, core.InlineDataset]) -> str: if values == [{}]: return "empty" values_json = json.dumps(values, sort_keys=True) - hsh = hashlib.md5(values_json.encode()).hexdigest() + hsh = hashlib.sha256(values_json.encode()).hexdigest()[:32] return "data-" + hsh diff --git a/altair/vegalite/v5/display.py b/altair/vegalite/v5/display.py index 93926e62b8..b13d62e06c 100644 --- a/altair/vegalite/v5/display.py +++ b/altair/vegalite/v5/display.py @@ -86,6 +86,15 @@ def svg_renderer(spec: dict, **metadata) -> Dict[str, str]: ) +def jupyter_renderer(spec: dict): + """Render chart using the JupyterChart Jupyter Widget""" + from altair import Chart, JupyterChart + + # Need to ignore attr-defined mypy rule because mypy doesn't see _repr_mimebundle_ + # conditionally defined in AnyWidget + return JupyterChart(chart=Chart.from_dict(spec))._repr_mimebundle_() # type: ignore[attr-defined] + + html_renderer = HTMLRenderer( mode="vega-lite", template="universal", @@ -105,6 +114,7 @@ def svg_renderer(spec: dict, **metadata) -> Dict[str, str]: renderers.register("json", json_renderer) renderers.register("png", png_renderer) renderers.register("svg", svg_renderer) +renderers.register("jupyter", jupyter_renderer) renderers.enable("default") diff --git a/doc/about/code_of_conduct.rst b/doc/about/code_of_conduct.rst index e4c1dbdcbc..a77c425f53 100644 --- a/doc/about/code_of_conduct.rst +++ b/doc/about/code_of_conduct.rst @@ -1,127 +1,4 @@ Code of Conduct =============== -Our Pledge ----------- -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -Our Standards -------------- -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of - any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, - without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -Enforcement Responsibilities ----------------------------- -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -Scope ------ -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -Enforcement ------------ -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -vegajs.conduct@gmail.com. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -Enforcement Guidelines ----------------------- -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -1. Correction -~~~~~~~~~~~~~ -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -2. Warning -~~~~~~~~~~ -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -3. Temporary Ban -~~~~~~~~~~~~~~~~ -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -4. Permanent Ban -~~~~~~~~~~~~~~~~ -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -Attribution ------------ -This Code of Conduct is adapted from the `Contributor Covenant `_, -version 2.1, available at -`https://www.contributor-covenant.org/version/2/1/code_of_conduct.html `_. - -Community Impact Guidelines were inspired by -`Mozilla's code of conduct enforcement ladder `_. - -For answers to common questions about this code of conduct, see the FAQ at -`https://www.contributor-covenant.org/faq `_. Translations are available at -`https://www.contributor-covenant.org/translations `_. +As a project of the Vega Organization, we use the `Vega Code of Conduct `. diff --git a/doc/about/governance.rst b/doc/about/governance.rst index 3b467a91aa..8be1611969 100644 --- a/doc/about/governance.rst +++ b/doc/about/governance.rst @@ -6,11 +6,10 @@ Vega-Altair's governance structure is based on GitHub's Organizational Governance ------------------------- The Altair-Viz organization is governed by the documents that reside in the -`governance/org-docs `_ directory of the -Vega-Altair GitHub repository. +`Vega Organizational GitHub repository `_. Project Governance ------------------ The Vega-Altair library is governed by the documents that reside in the -`governance/project-docs `_ directory of -the Vega-Altair GitHub repository. \ No newline at end of file +`project-docs `_ directory of +the Vega Organizational GitHub repository. \ No newline at end of file diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst index 206e7f8c33..4329755140 100644 --- a/doc/releases/changes.rst +++ b/doc/releases/changes.rst @@ -8,10 +8,17 @@ Version 5.3.0 (unreleased month day, year) Enhancements ~~~~~~~~~~~~ +- Add "jupyter" renderer which uses JupyterChart for rendering (#3283). See :ref:`renderers` for more information. +- Docs: Add :ref:`section on dashboards ` which have support for Altair (#3299) +- Support restrictive FIPS-compliant environment (#3291) + Bug Fixes ~~~~~~~~~ +- Fix type hints for libraries such as Polars where Altair uses the dataframe interchange protocol (#3297) + Backward-Incompatible Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Changed hash function from ``md5`` to a truncated ``sha256`` non-cryptograhic hash (#3291) Version 5.2.0 (released Nov 28, 2023) ------------------------------------------- diff --git a/doc/user_guide/display_frontends.rst b/doc/user_guide/display_frontends.rst index ad418bea82..bb488986a2 100644 --- a/doc/user_guide/display_frontends.rst +++ b/doc/user_guide/display_frontends.rst @@ -40,6 +40,16 @@ The most used built-in renderers are: newer versions of JupyterLab_, nteract_, and `VSCode-Python`_, but does not work with the `Jupyter Notebook`_, or with tools like nbviewer_ and nbconvert_. +``alt.renderers.enable("jupyter")`` + *(added in version 5.3):* Output the chart using :ref:`user-guide-jupyterchart`. This renderer + is compatible with environments that support third-party Jupyter Widgets including + JupyterLab_, `Jupyter Notebook`_, `VSCode-Python`_, and `Colab`_. + It requires a web connection in order to load relevant Javascript libraries. Note that, + although this renderer uses ``JupyterChart``, it does not provide the + ability to access value and selection params in Python. To do so, create a ``JupyterChart`` + object explicitly following the instructions in the :ref:`user-guide-jupyterchart` + documentation. + In addition, Altair includes the following renderers: - ``"default"``, ``"colab"``, ``"kaggle"``, ``"zeppelin"``: identical to ``"html"`` @@ -135,10 +145,33 @@ Optionally, for offline rendering, you can use the mimetype renderer:: # Optional in VS Code alt.renderers.enable('mimetype') +.. _display_dashboards: + +Dashboards +---------- +Altair is compatible with common Python dashboarding packages. Many of them even provide support for reading out :ref:`parameters ` from the chart. +This allows you to e.g. select data points and update another part of the dashboard such as a table based on that selection: + +=================================================================================================================================== =================================== ============================= +Package Displays interactive Altair charts Supports reading out parameters +=================================================================================================================================== =================================== ============================= +`Panel `_ ✔ ✔ +`Plotly Dash `_ using `dash_vega_components `_ ✔ ✔ +`Jupyter Voila `_ using :ref:`JupyterChart ` ✔ ✔ +`Shiny `_ using :ref:`JupyterChart ` ✔ ✔ +`Solara `_ ✔ ✔ +`Streamlit `_ ✔ +=================================================================================================================================== =================================== ============================= + +The above mentioned frameworks all require you to run a web application on a server if you want to share your work with others. A web application gives you a lot of flexibility, you can for example fetch data from a database based on the value of a dropdown menu in the dashboard. However, it comes with some complexity as well. +For use cases where the interactivity provided by Altair itself is enough, you can also use tools which generate HTML pages which do not require a web server such as `Quarto `_ or `Jupyter Book `_. + +If you are using a dashboarding package that is not listed here, please `open an issue `_ on GitHub so that we can add it. + .. _display-general: -Working in non-Notebook Environments ------------------------------------- +Working in environments without a JavaScript frontend +----------------------------------------------------- The Vega-Lite specifications produced by Altair can be produced in any Python environment, but to render these specifications currently requires a javascript engine. For this reason, Altair works most seamlessly with the browser-based @@ -150,16 +183,6 @@ to a second tool that can execute javascript. There are a few options available for this: -Vega-enabled IDEs -~~~~~~~~~~~~~~~~~ -Some IDEs have extensions that natively recognize and display Altair charts. -Examples are: - -- The `VSCode-Python`_ extension, which supports native Altair and Vega-Lite - chart display as of November 2019. -- The Hydrogen_ project, which is built on nteract_ and renders Altair charts - via the ``mimetype`` renderer. - Altair Viewer ~~~~~~~~~~~~~ .. note:: diff --git a/doc/user_guide/jupyter_chart.rst b/doc/user_guide/jupyter_chart.rst index 92ac6944c3..d2bcaa63a9 100644 --- a/doc/user_guide/jupyter_chart.rst +++ b/doc/user_guide/jupyter_chart.rst @@ -1,7 +1,7 @@ .. _user-guide-jupyterchart: -JupyterChart Interactivity -========================== +JupyterChart +============ The ``JupyterChart`` class, introduced in Vega-Altair 5.1, makes it possible to update charts after they have been displayed and access the state of :ref:`user-guide-interactions` from Python. diff --git a/doc/user_guide/large_datasets.rst b/doc/user_guide/large_datasets.rst index 18376da41d..39c5f8756f 100644 --- a/doc/user_guide/large_datasets.rst +++ b/doc/user_guide/large_datasets.rst @@ -90,7 +90,9 @@ unused columns, which reduces dataset size even for charts without data transfor When the ``"vegafusion"`` data transformer is active, data transformations will be pre-evaluated when :ref:`displaying-charts`, :ref:`user-guide-saving`, converted charts a dictionaries, -and converting charts to JSON. +and converting charts to JSON. When combined with :ref:`user-guide-jupyterchart` or the ``"jupyter"`` +renderer (See :ref:`customizing-renderers`), data transformations will also be evaluated in Python +dynamically in response to chart selection events. VegaFusion's development is sponsored by `Hex `_. @@ -108,8 +110,6 @@ or conda conda install -c conda-forge vegafusion vegafusion-python-embed vl-convert-python -Note that conda packages are not yet available for the Apple Silicon architecture. - Enabling the VegaFusion Data Transformer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Activate the VegaFusion data transformer with: @@ -123,10 +123,11 @@ All charts created after activating the VegaFusion data transformer will work with datasets containing up to 100,000 rows. VegaFusion's row limit is applied after all supported data transformations have been applied. So you are unlikely to reach it with a chart such as a histogram, -but you may hit it in the case of a large scatter chart or a chart that uses interactivity. -If you need to work with larger datasets, -you can disable the maximum row limit -or switch to using the VegaFusion widget renderer described below. +but you may hit it in the case of a large scatter chart or a chart that includes interactivity +when not using ``JupyterChart`` or the ``"jupyter"`` renderer. + +If you need to work with larger datasets, you can disable the maximum row limit +or switch to using ``JupyterChart`` or the ``"jupyter"`` renderer described below. Converting to JSON or dictionary ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -148,8 +149,8 @@ Local Timezone Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some Altair transformations (e.g. :ref:`user-guide-timeunit-transform`) are based on a local timezone. Normally, the browser's local timezone is used. However, because -VegaFusion evaluates these transforms in Python before rendering, it's not possible to -access the browser's timezone. Instead, the local timezone of the Python kernel will be +VegaFusion evaluates these transforms in Python before rendering, it's not always possible +to access the browser's timezone. Instead, the local timezone of the Python kernel will be used by default. In the case of a cloud notebook service, this may be difference than the browser's local timezone. @@ -161,6 +162,9 @@ function. For example: import vegafusion as vf vf.set_local_tz("America/New_York") +When using ``JupyterChart`` or the ``"jupyter"`` renderer, the browser's local timezone +is used. + DuckDB Integration ^^^^^^^^^^^^^^^^^^ VegaFusion provides optional integration with `DuckDB`_. Because DuckDB can perform queries on pandas @@ -169,25 +173,32 @@ which requires this conversion. See the `VegaFusion DuckDB`_ documentation for m Interactivity ^^^^^^^^^^^^^ -For charts that use selections to filter data interactively, the VegaFusion data transformer -will include all of the data that participates in the interaction in the resulting chart -specification. This makes it an unsuitable approach for building interactive charts that filter -large datasets (e.g. crossfiltering a dataset with over a million rows). +When using the default ``"html"`` renderer with charts that use selections to filter data interactively, +the VegaFusion data transformer will include all of the data that participates in the interaction in the resulting chart specification. This makes it an unsuitable approach for building interactive charts that filter large datasets (e.g. crossfiltering a dataset with over a million rows). -The `VegaFusion widget renderer`_ is designed to support this use case, and is available in the -third-party ``vegafusion-jupyter`` package. +The ``JupyterChart`` widget and the ``"jupyter"`` renderer are designed to work with the VegaFusion +data transformer to evaluate data transformations interactively in response to selection events. +This avoids the need to transfer the full dataset to the browser, and so supports +interactive exploration of aggregated datasets on the order of millions of rows. -It is enabled with: +Either use ``JupyterChart`` directly: .. code-block:: python - import vegafusion as vf - vf.enable_widget() + import altair as alt + alt.data_transformers.enable("vegafusion") + ... + alt.JupyterChart(chart) -The widget renderer uses a Jupyter Widget extension to maintain a live connection between the displayed chart -and the Python kernel. This makes it possible for transforms to be evaluated interactively in response to -changes in selections, and to send the datasets to the client in arrow format separately instead of inlining -them in the chart json specification. +Or, enable the ``"jupyter"`` renderer and display charts as usual: + +.. code-block:: python + + import altair as alt + alt.data_transformers.enable("vegafusion") + alt.renderers.enable("jupyter") + ... + chart Charts rendered this way require a running Python kernel and Jupyter Widget extension to display, which works in many frontends including locally in the classic notebook, JupyterLab, and VSCode, @@ -455,8 +466,6 @@ summary statistics to Altair instead of the full dataset. rules + bars + ticks + outliers .. _VegaFusion: https://vegafusion.io -.. _VegaFusion mime renderer: https://vegafusion.io/mime_renderer.html -.. _VegaFusion widget renderer: https://vegafusion.io/widget_renderer.html .. _DuckDB: https://duckdb.org/ .. _VegaFusion DuckDB: https://vegafusion.io/duckdb.html .. _vl-convert: https://github.com/vega/vl-convert diff --git a/governance/org-docs/ANTITRUST.md b/governance/org-docs/ANTITRUST.md deleted file mode 100644 index 6d734c57f0..0000000000 --- a/governance/org-docs/ANTITRUST.md +++ /dev/null @@ -1,7 +0,0 @@ -# Antitrust Policy - -Participants acknowledge that they may compete with other participants in various lines of business and that it is therefore imperative that they and their respective representatives act in a manner that does not violate any applicable antitrust laws, competition laws, or associated regulations. This Policy does not restrict any participant from engaging in other similar projects. Each participant may design, develop, manufacture, acquire or market competitive deliverables, products, and services, and conduct its business, in whatever way it chooses. No participant is obligated to announce or market any products or services. Without limiting the generality of the foregoing, participants agree not to have any discussion relating to any product pricing, methods or channels of product distribution, contracts with third-parties, division or allocation of markets, geographic territories, or customers, or any other topic that relates in any way to limiting or lessening fair competition. - ---- -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/governance/org-docs/CHARTER.md b/governance/org-docs/CHARTER.md deleted file mode 100644 index 1a980bd86b..0000000000 --- a/governance/org-docs/CHARTER.md +++ /dev/null @@ -1,67 +0,0 @@ -# Charter for the Altair-Viz Organization - -This is the organizational charter for the Altair-Viz Organization (the "Organization"). By adding their name to the [Steering Committee.md file](./STEERING-COMMITTEE.md), Steering Committee members agree as follows. - -## 1. Terminology -* "Altair-Viz": The GitHub organization at https://github.com/altair-viz -* "Vega-Altair" and "Altair": The data visualization library developed at https://github.com/altair-viz/altair. When referring to the library in a standalone context such as in talks, article titles, or when being compared to other Python visualization libraries, it is preferrable to use "Vega-Altair". You can refer to the shorter "Altair" within a context where the project has already been introduced as Vega-Altair such as in the body of a talk or article. - -## 2. Mission - -Altair-Viz builds visualization tools with the main focus on Vega-Altair which is a declarative visualization library in Python. Its simple, friendly and consistent API, built on top of the powerful Vega-Lite grammar, empowers users to spend less time writing code and more time exploring their data. - -## 3. Steering Committee - -**2.1 Purpose**. The Steering Committee will be responsible for all technical oversight, project approval and oversight, policy oversight, and trademark management for the Organization. - -**2.2 Composition**. The Steering Committee voting members are listed in the steering-committee.md file in the repository. -Voting members may be added or removed by no less than 3/4 affirmative vote of the Steering Committee. -The Steering Committee will appoint a Chair responsible for organizing Steering Committee activity. - -## 4. Voting - -**3.1. Decision Making**. The Steering Committee will strive for all decisions to be made by consensus. While explicit agreement of the entire Steering Committee is preferred, it is not required for consensus. Rather, the Steering Committee will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Steering Committee and nature of support and objections. The Steering Committee will document evidence of consensus in accordance with these requirements. If consensus cannot be reached, the Steering Committee will make the decision by a vote. - -**3.2. Voting**. The Steering Committee Chair will call a vote with reasonable notice to the Steering Committee, setting out a discussion period and a separate voting period. Any discussion may be conducted in person or electronically by text, voice, or video. The discussion will be open to the public. In any vote, each voting representative will have one vote. Except as specifically noted elsewhere in this Charter, decisions by vote require a simple majority vote of all voting members. - -## 5. Termination of Membership - -In addition to the method set out in section 2.2, the membership of a Steering Committee member will terminate if any of the following occur: - -**4.1 Resignation**. Written notice of resignation to the Steering Committee. - -**4.2 Unreachable Member**. If a member is unresponsive at its listed handle for more than three months the Steering Committee may vote to remove the member. - -**4.3 Code of Conduct violation**. If a member violates the Code of Conduct in a way that justifies at least a warning, the Steering Committee may vote to remove the member. - -## 6. Trademarks - -Any names, trademarks, service marks, logos, mascots, or similar indicators of source or origin and the goodwill associated with them arising out of the Organization's activities or Organization projects' activities (the "Marks"), are controlled by the Organization. Steering Committee members may only use the Marks in accordance with the Organization's [trademark policy](./TRADEMARKS.md). If a Steering Committee member is terminated or removed from the Steering Committee, any rights the Steering Committee member may have in the Marks revert to the Organization. - -## 7. Antitrust Policy - -The Steering Committee is bound by the Organization's [antitrust policy](./ANTITRUST.md). - -## 8. No Confidentiality - -Information disclosed in connection with any of the Organization's activities, including but not limited to meetings, Contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. - -## 9. Project Criteria - -In order to be eligible to be a Organization project, a project must: - -* Be approved by the Steering Committee. -* Agree to follow the guidance and direction of the Steering Committee. -* Use only the following outbound licenses or agreements unless otherwise approved: - - For code, a license on the Open Source Initiative's list of [Popular Licenses](https://opensource.org/licenses). - - For data, a license on the Open Knowledge Foundation's list of [Recommended Conformant Licenses](http://opendefinition.org/licenses/). - - For specifications, a community developed and maintained specification agreement, such the [Open Web Foundation Agreements](https://www.openwebfoundation.org/the-agreements) or [Community Specification Agreement](https://github.com/CommunitySpecification/1.0). -* Include and adhere to the Organization's policies, including the [trademark policy](./TRADEMARKS.md), the [antitrust policy](./ANTITRUST.md), and the [code of conduct](./CODE-OF-CONDUCT.md). - -## 10. Amendments - -Amendments to this charter, the [antitrust policy](./ANTITRUST.md), the [trademark policy](./TRADEMARKS.md), or the [code of conduct](./CODE-OF-CONDUCT.md) may only be made with at least a 3/4 affirmative vote of the Steering Committee. - ---- -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/governance/org-docs/CODE-OF-CONDUCT.md b/governance/org-docs/CODE-OF-CONDUCT.md deleted file mode 100644 index 906721cbd0..0000000000 --- a/governance/org-docs/CODE-OF-CONDUCT.md +++ /dev/null @@ -1,2 +0,0 @@ -# Contributor Covenant Code of Conduct -See the [CODE_OF_CONDUCT.md file](https://github.com/altair-viz/altair/blob/main/CODE_OF_CONDUCT.md) in the altair repository. \ No newline at end of file diff --git a/governance/org-docs/STEERING-COMMITTEE.md b/governance/org-docs/STEERING-COMMITTEE.md deleted file mode 100644 index 67a998c9b2..0000000000 --- a/governance/org-docs/STEERING-COMMITTEE.md +++ /dev/null @@ -1,15 +0,0 @@ -# Steering Committee - -This document lists the members of the Organization's Steering Committee. Voting members may be added once approved by the Steering Committee as described in the [charter](./CHARTER.md). By adding your name to this list you are agreeing to abide by all Organization polices, including the [charter](./CHARTER.md), the [code of conduct](./CODE-OF-CONDUCT.md), the [trademark policy](./TRADEMARKS.md), and the [antitrust policy](./ANTITRUST.md). If you are serving on the Steering Committee because of your affiliation with another organization (designated below), you represent that you have authority to bind that organization to these policies. - -| **NAME** | **Handle** | **Affiliated Organization** | -| --- | --- | --- | -| Stefan Binder (Chair) | @binste | - | -| Joel Ostblom | @joelostblom | - | -| Jon Mease | @jonmmease | [Hex Technologies](https://hex.tech/) | -| Mattijn van Hoek | @mattijn | - | -| Christopher Davis | @ChristopherDavisUCI | - | - ---- -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/governance/org-docs/TRADEMARKS.md b/governance/org-docs/TRADEMARKS.md deleted file mode 100644 index d58e223eba..0000000000 --- a/governance/org-docs/TRADEMARKS.md +++ /dev/null @@ -1,44 +0,0 @@ -## Introduction - -This is the Organization's policy for the use of our trademarks. While our work is available under free and open source software licenses, those licenses do not include a license to use our trademarks. - -This policy describes how you may use our trademarks. Our goal is to strike a balance between: 1) our need to ensure that our trademarks remain reliable indicators of the quality software we release; and 2) our community members' desire to be full participants in our Organization. - -## Our Trademarks - -This policy covers the name of the Organization and each of the Organization's projects, as well as any associated names, trademarks, service marks, logos, mascots, or similar indicators of source or origin (our "Marks"). - -## In General - -Whenever you use our Marks, you must always do so in a way that does not mislead anyone about exactly who is the source of the software. For example, you cannot say you are distributing the "Mark" software when you're distributing a modified version of it because people will believe they are getting the same software that they can get directly from us when they aren't. You also cannot use our Marks on your website in a way that suggests that your website is an official Organization website or that we endorse your website. But, if true, you can say you like the "Mark" software, that you participate in the "Mark" community, that you are providing an unmodified version of the "Mark" software, or that you wrote a book describing how to use the "Mark" software. - -This fundamental requirement, that it is always clear to people what they are getting and from whom, is reflected throughout this policy. It should also serve as your guide if you are not sure about how you are using the Marks. - -In addition: -* You may not use or register, in whole or in part, the Marks as part of your own trademark, service mark, domain name, company name, trade name, product name or service name. -* Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our Marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. -* You agree that any goodwill generated by your use of the Marks and participation in our community inures solely to our collective benefit. - -## Distribution of unmodified source code or unmodified executable code we have compiled - -When you redistribute an unmodified copy of our software, you are not changing the quality or nature of it. Therefore, you may retain the Marks we have placed on the software to identify your redistribution. This kind of use only applies if you are redistributing an official distribution from this Project that has not been changed in any way. - -## Distribution of executable code that you have compiled, or modified code - -You may use any word marks, but not any Organization logos, to truthfully describe the origin of the software that you are providing, that is, that the code you are distributing is a modification of our software. You may say, for example, that "this software is derived from the source code for 'Mark' software." - -Of course, you can place your own trademarks or logos on versions of the software to which you have made substantive modifications, because by modifying the software, you have become the origin of that exact version. In that case, you should not use our Marks. - -However, you may use our Marks for the distribution of code (source or executable) on the condition that any executable is built from the official Project source code and that any modifications are limited to switching on or off features already included in the software, translations into other languages, and incorporating minor bug-fix patches. Use of our Marks on any further modification is not permitted. - -## Statements about your software's relation to our software - -You may use the word Marks, but not the Organization's logos, to truthfully describe the relationship between your software and ours. Our Mark should be used after a verb or preposition that describes the relationship between your software and ours. So you may say, for example, "Bob's software for the 'Mark' platform" but may not say "Bob's 'Mark' software." Some other examples that may work for you are: - -* [Your software] uses "Mark" software -* [Your software] is powered by "Mark" software -* [Your software] runs on "Mark" software -* [Your software] for use with "Mark" software -* [Your software] for Mark software - -These guidelines are based on the [Model Trademark Guidelines](http://www.modeltrademarkguidelines.org), used under a [Creative Commons Attribution 3.0 Unported license](https://creativecommons.org/licenses/by/3.0/deed.en_US) diff --git a/governance/project-docs/CONTRIBUTING.md b/governance/project-docs/CONTRIBUTING.md deleted file mode 100644 index 61460b18a9..0000000000 --- a/governance/project-docs/CONTRIBUTING.md +++ /dev/null @@ -1,2 +0,0 @@ -# Contributing -See the [CONTRIBUTING.md file](https://github.com/altair-viz/altair/blob/main/CONTRIBUTING.md) in the altair repository. \ No newline at end of file diff --git a/governance/project-docs/GOVERNANCE.md b/governance/project-docs/GOVERNANCE.md deleted file mode 100644 index 1ed37f65b6..0000000000 --- a/governance/project-docs/GOVERNANCE.md +++ /dev/null @@ -1,45 +0,0 @@ -# Governance Policy - -This document provides the governance policy for the Project. Maintainers agree to this policy and to abide by all Project polices, including the [code of conduct](../org-docs/CODE-OF-CONDUCT.md), [trademark policy](../org-docs/TRADEMARKS.md), and [antitrust policy](../org-docs/ANTITRUST.md) by adding their name to the [maintainers.md file](./MAINTAINERS.md). - -## 1. Roles. - -This project may include the following roles. Additional roles may be adopted and documented by the Project. - -**1.1. Maintainers**. Maintainers are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are also responsible for determining consensus. This Project may add or remove Maintainers with the approval of the current Maintainers. - -**1.2. Contributors**. Contributors are those that have made contributions to the Project. - -## 2. Decisions. - -**2.1. Consensus-Based Decision Making**. Projects make decisions through consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Contributors and nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements. - -**2.2. Appeal Process**. Decisions may be appealed by opening an issue and that appeal will be considered by the Maintainers in good faith, who will respond in writing within a reasonable time. If the Maintainers deny the appeal, the appeal may be brought before the Organization Steering Committee, who will also respond in writing in a reasonable time. - -## 3. How We Work. - -**3.1. Openness**. Participation is open to anyone who is directly and materially affected by the activity in question. There shall be no undue financial barriers to participation. - -**3.2. Balance**. The development process should balance the interests of Contributors and other stakeholders. Contributors from diverse interest categories shall be sought with the objective of achieving balance. - -**3.3. Coordination and Harmonization**. Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in this Project. - -**3.4. Consideration of Views and Objections**. Prompt consideration shall be given to the written views and objections of all Contributors. - -**3.5. Written procedures**. This governance document and other materials documenting this project's development process shall be available to any interested person. - -## 4. No Confidentiality. - -Information disclosed in connection with any Project activity, including but not limited to meetings, contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. - -## 5. Trademarks. - -Any names, trademarks, logos, or goodwill developed by and associated with the Project (the "Marks") are controlled by the Organization. Maintainers may only use these Marks in accordance with the Organization's trademark policy. If a Maintainer resigns or is removed, any rights the Maintainer may have in the Marks revert to the Organization. - -## 6. Amendments. - -Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee. - ---- -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/governance/project-docs/LICENSE.md b/governance/project-docs/LICENSE.md deleted file mode 100644 index dacb83d204..0000000000 --- a/governance/project-docs/LICENSE.md +++ /dev/null @@ -1,2 +0,0 @@ -# License -See the [LICENSE file](https://github.com/altair-viz/altair/blob/main/LICENSE) in the altair repository. \ No newline at end of file diff --git a/governance/project-docs/MAINTAINERS.md b/governance/project-docs/MAINTAINERS.md deleted file mode 100644 index b4417583e6..0000000000 --- a/governance/project-docs/MAINTAINERS.md +++ /dev/null @@ -1,15 +0,0 @@ -# Maintainers - -This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](./GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](../org-docs/CODE-OF-CONDUCT.md), [trademark policy](../org-docs/TRADEMARKS.md), and [antitrust policy](../org-docs/ANTITRUST.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies. - -| **NAME** | **Handle** | **Affiliated Organization** | -| --- | --- | --- | -| Stefan Binder | @binste | - | -| Joel Ostblom | @joelostblom | - | -| Jon Mease | @jonmmease | [Hex Technologies](https://hex.tech/) | -| Mattijn van Hoek | @mattijn | - | -| Christopher Davis | @ChristopherDavisUCI | - | - ---- -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/pyproject.toml b/pyproject.toml index 0b7c46de24..ee90975b47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ dev = [ "types-jsonschema", "types-setuptools", "pyarrow>=11", - "vegafusion[embed]>=1.4.0", + "vegafusion[embed]>=1.5.0", "anywidget", "geopandas", ] diff --git a/sphinxext/altairgallery.py b/sphinxext/altairgallery.py index f76805c90d..a6310ddd9f 100644 --- a/sphinxext/altairgallery.py +++ b/sphinxext/altairgallery.py @@ -166,7 +166,7 @@ def save_example_pngs(examples, image_dir, make_thumbnails=True): filename = example["name"] + (".svg" if example["use_svg"] else ".png") image_file = os.path.join(image_dir, filename) - example_hash = hashlib.md5(example["code"].encode()).hexdigest() + example_hash = hashlib.sha256(example["code"].encode()).hexdigest()[:32] hashes_match = hashes.get(filename, "") == example_hash if hashes_match and os.path.exists(image_file): diff --git a/sphinxext/utils.py b/sphinxext/utils.py index c3bd2052d8..50d17608cf 100644 --- a/sphinxext/utils.py +++ b/sphinxext/utils.py @@ -193,8 +193,8 @@ def dict_hash(dct): serialized = json.dumps(dct, sort_keys=True) try: - m = hashlib.md5(serialized) + m = hashlib.sha256(serialized)[:32] except TypeError: - m = hashlib.md5(serialized.encode()) + m = hashlib.sha256(serialized.encode())[:32] return m.hexdigest() diff --git a/tests/examples_arguments_syntax/histogram_gradient_color.py b/tests/examples_arguments_syntax/histogram_gradient_color.py new file mode 100644 index 0000000000..6bd3e7b6ec --- /dev/null +++ b/tests/examples_arguments_syntax/histogram_gradient_color.py @@ -0,0 +1,23 @@ +""" +Histogram with Gradient Color +----------------------------- +This example shows how to make a histogram with gradient color. +The low-high IMDB rating is represented with the color scheme `pinkyellowgreen`. +""" +# category: distributions +import altair as alt +from vega_datasets import data + +source = data.movies.url + +alt.Chart(source).mark_bar().encode( + alt.X("IMDB_Rating:Q", + bin=alt.Bin(maxbins=20), + scale=alt.Scale(domain=[1, 10]) + ), + alt.Y('count()'), + alt.Color("IMDB_Rating:Q", + bin=alt.Bin(maxbins=20), + scale=alt.Scale(scheme='pinkyellowgreen') + ) +) \ No newline at end of file diff --git a/tests/examples_methods_syntax/histogram_gradient_color.py b/tests/examples_methods_syntax/histogram_gradient_color.py new file mode 100644 index 0000000000..645453475d --- /dev/null +++ b/tests/examples_methods_syntax/histogram_gradient_color.py @@ -0,0 +1,17 @@ +""" +Histogram with Gradient Color +----------------------------- +This example shows how to make a histogram with gradient color. +The low-high IMDB rating is represented with the color scheme `pinkyellowgreen`. +""" +# category: distributions +import altair as alt +from vega_datasets import data + +source = data.movies.url + +alt.Chart(source).mark_bar().encode( + alt.X("IMDB_Rating:Q").bin(maxbins=20).scale(domain=[1, 10]), + alt.Y('count()'), + alt.Color("IMDB_Rating:Q").bin(maxbins=20).scale(scheme='pinkyellowgreen') +) \ No newline at end of file diff --git a/tests/test_jupyter_chart.py b/tests/test_jupyter_chart.py index 3eebcc9dc3..d6d7815a3f 100644 --- a/tests/test_jupyter_chart.py +++ b/tests/test_jupyter_chart.py @@ -30,6 +30,9 @@ def test_chart_with_no_interactivity(transformer): widget = alt.JupyterChart(chart) if transformer == "vegafusion": + # With the "vegafusion" transformer, the spec is not computed until the front-end + # sets the local_tz. Assign this property manually to simulate this. + widget.local_tz = "UTC" assert widget.spec == chart.to_dict(format="vega") else: assert widget.spec == chart.to_dict() @@ -59,6 +62,7 @@ def test_interval_selection_example(transformer): widget = alt.JupyterChart(chart) if transformer == "vegafusion": + widget.local_tz = "UTC" assert widget.spec == chart.to_dict(format="vega") else: assert widget.spec == chart.to_dict() @@ -126,6 +130,7 @@ def test_index_selection_example(transformer): widget = alt.JupyterChart(chart) if transformer == "vegafusion": + widget.local_tz = "UTC" assert widget.spec == chart.to_dict(format="vega") else: assert widget.spec == chart.to_dict() @@ -185,6 +190,7 @@ def test_point_selection(transformer): widget = alt.JupyterChart(chart) if transformer == "vegafusion": + widget.local_tz = "UTC" assert widget.spec == chart.to_dict(format="vega") else: assert widget.spec == chart.to_dict() diff --git a/tests/vegalite/v5/test_api.py b/tests/vegalite/v5/test_api.py index 638eaac461..af963be7d9 100644 --- a/tests/vegalite/v5/test_api.py +++ b/tests/vegalite/v5/test_api.py @@ -382,14 +382,7 @@ def test_save_html(basic_chart, inline): def test_to_url(basic_chart): share_url = basic_chart.to_url() - expected_vegalite_encoding = ( - "N4Igxg9gdgZglgcxALlANzgUwO4tJKAFzigFcJSBnAdTgBNCALFAZgAY2AacaYsiygAlMiRoVYcAvpO5" - "0AhoTl4QUOQFtMKEPMUBaMACY5LTAA4AnACM55ugFY6ARgBspgOz2zh03Wfs5bCwsIDIganIATgDWyoQ" - "AngAOmsgg1hEh3JhQkHQkSKggAB7K8JgANnRaStzxSVpQEGokcmUZIHElWBValiA1ickgAI6kckRwisR" - "omtLcACSUYIyY4VpihAmUyAD029MIcgB0CBOMpJaHcBDbi8vhe5gHumUTmHt2hy6HLIcAVpTQPraBRyS" - "iYQiUZQ6OT6IwmCzWWwOFzuTymby+fyBYLIADaoCUKQAgkDesgDKYZAStAAhUkoOx2KkgQkgADC9OQABY" - "WMzWQARTnmRx8rQAUU5phFnGpKQAYpy7LyZSytABxTmOcyilKCSVuHUgACSioMkgAutIgA" - ) + expected_vegalite_encoding = "N4Igxg9gdgZglgcxALlANzgUwO4tJKAFzigFcJSBnAdTgBNCALFAZgAY2AacaYsiygAlMiRoVYcAvpO50AhoTl4QUOQFtMKEPMUBaAOwA2ABwAWFi1NyTcgEb7TtuabAswc-XTZhMczLdNDAEYQGRA1OQAnAGtlQgBPAAdNZBAnSNDuTChIOhIkVBAAD2V4TAAbOi0lbgTkrSgINRI5csyQeNKsSq1bEFqklJAAR1I5IjhFYjRNaW4AEkowRkwIrTFCRMpkAHodmYQ5ADoEScZSWyO4CB2llYj9zEPdcsnMfYBWI6DDI5YjgBWlGg-W0CjklEwhEoyh0cgMJnMlmsxjsDicLjcHi8Pj8AWCKAA2qAlKkAIKgvrIABMxhkJK0ACFKSgPh96SBSSAAMIs5DmDlcgAifIAnEFBVoAKJ84wSzgM1IAMT5HxYktSAHE+UFRRqQIJZfp9QBJVXUyQAXWkQA" assert ( share_url