Skip to content

Commit

Permalink
Support configurable renderer in html export (#141)
Browse files Browse the repository at this point in the history
* Support configurable renderer in html export

* clippy fix

* fix html Python tests
  • Loading branch information
jonmmease authored Dec 2, 2023
1 parent ef44149 commit 4887a5b
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 25 deletions.
25 changes: 15 additions & 10 deletions vl-convert-python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#![allow(clippy::too_many_arguments)]

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict};
use pythonize::{depythonize, pythonize};
use std::str::FromStr;
use std::sync::Mutex;
use vl_convert_rs::converter::{FormatLocale, TimeFormatLocale, VgOpts, VlOpts};
use vl_convert_rs::converter::{FormatLocale, Renderer, TimeFormatLocale, VgOpts, VlOpts};
use vl_convert_rs::html::bundle_vega_snippet;
use vl_convert_rs::module_loader::import_map::VlVersion;
use vl_convert_rs::module_loader::{FORMATE_LOCALE_MAP, TIME_FORMATE_LOCALE_MAP};
Expand Down Expand Up @@ -190,7 +192,6 @@ fn vega_to_scenegraph(
/// time_format_locale (str | dict): d3-time-format locale name or dictionary
/// Returns:
/// str: SVG image string
#[allow(clippy::too_many_arguments)]
#[pyfunction]
#[pyo3(
text_signature = "(vl_spec, vl_version, config, theme, show_warnings, allowed_base_urls, format_locale, time_format_locale)"
Expand Down Expand Up @@ -259,7 +260,6 @@ fn vegalite_to_svg(
/// time_format_locale (str | dict): d3-time-format locale name or dictionary
/// Returns:
/// str: SVG image string
#[allow(clippy::too_many_arguments)]
#[pyfunction]
#[pyo3(
text_signature = "(vl_spec, vl_version, config, theme, show_warnings, allowed_base_urls, format_locale, time_format_locale)"
Expand Down Expand Up @@ -392,7 +392,6 @@ fn vega_to_png(
#[pyo3(
text_signature = "(vl_spec, vl_version, scale, ppi, config, theme, show_warnings, allowed_base_urls, format_locale, time_format_locale)"
)]
#[allow(clippy::too_many_arguments)]
fn vegalite_to_png(
vl_spec: PyObject,
vl_version: Option<&str>,
Expand Down Expand Up @@ -525,7 +524,6 @@ fn vega_to_jpeg(
#[pyo3(
text_signature = "(vl_spec, vl_version, scale, quality, config, theme, show_warnings, allowed_base_urls, format_locale, time_format_locale)"
)]
#[allow(clippy::too_many_arguments)]
fn vegalite_to_jpeg(
vl_spec: PyObject,
vl_version: Option<&str>,
Expand Down Expand Up @@ -646,7 +644,6 @@ fn vega_to_pdf(
/// time_format_locale (str | dict): d3-time-format locale name or dictionary
/// Returns:
/// bytes: PDF image data
#[allow(clippy::too_many_arguments)]
#[pyfunction]
#[pyo3(
text_signature = "(vl_spec, vl_version, scale, config, theme, allowed_base_urls, format_locale, time_format_locale)"
Expand Down Expand Up @@ -750,11 +747,13 @@ fn vega_to_url(vg_spec: PyObject, fullscreen: Option<bool>) -> PyResult<String>
/// theme (str | None): Named theme (e.g. "dark") to apply during conversion
/// format_locale (str | dict): d3-format locale name or dictionary
/// time_format_locale (str | dict): d3-time-format locale name or dictionary
/// renderer (str): Vega renderer. One of 'svg' (default), 'canvas',
/// or 'hybrid' (where text is svg and other marks are canvas)
/// Returns:
/// string: HTML document
#[pyfunction]
#[pyo3(
text_signature = "(vl_spec, vl_version, bundle, config, theme, format_locale, time_format_locale)"
text_signature = "(vl_spec, vl_version, bundle, config, theme, format_locale, time_format_locale, renderer)"
)]
fn vegalite_to_html(
vl_spec: PyObject,
Expand All @@ -764,6 +763,7 @@ fn vegalite_to_html(
theme: Option<String>,
format_locale: Option<PyObject>,
time_format_locale: Option<PyObject>,
renderer: Option<String>,
) -> PyResult<String> {
let vl_version = if let Some(vl_version) = vl_version {
VlVersion::from_str(vl_version)?
Expand All @@ -774,7 +774,7 @@ fn vegalite_to_html(
let config = config.and_then(|c| parse_json_spec(c).ok());
let format_locale = parse_option_format_locale(format_locale)?;
let time_format_locale = parse_option_time_format_locale(time_format_locale)?;

let renderer = renderer.unwrap_or_else(|| "svg".to_string());
let mut converter = VL_CONVERTER
.lock()
.expect("Failed to acquire lock on Vega-Lite converter");
Expand All @@ -791,6 +791,7 @@ fn vegalite_to_html(
time_format_locale,
},
bundle.unwrap_or(false),
Renderer::from_str(&renderer)?,
))?)
}

Expand All @@ -802,20 +803,23 @@ fn vegalite_to_html(
/// If False (default), HTML file will load dependencies from only CDN
/// format_locale (str | dict): d3-format locale name or dictionary
/// time_format_locale (str | dict): d3-time-format locale name or dictionary
/// renderer (str): Vega renderer. One of 'svg' (default), 'canvas',
/// or 'hybrid' (where text is svg and other marks are canvas)
/// Returns:
/// string: HTML document
#[pyfunction]
#[pyo3(text_signature = "(vg_spec, bundle, format_locale, time_format_locale)")]
#[pyo3(text_signature = "(vg_spec, bundle, format_locale, time_format_locale, renderer)")]
fn vega_to_html(
vg_spec: PyObject,
bundle: Option<bool>,
format_locale: Option<PyObject>,
time_format_locale: Option<PyObject>,
renderer: Option<String>,
) -> PyResult<String> {
let vg_spec = parse_json_spec(vg_spec)?;
let format_locale = parse_option_format_locale(format_locale)?;
let time_format_locale = parse_option_time_format_locale(time_format_locale)?;

let renderer = renderer.unwrap_or_else(|| "svg".to_string());
let mut converter = VL_CONVERTER
.lock()
.expect("Failed to acquire lock on Vega-Lite converter");
Expand All @@ -827,6 +831,7 @@ fn vega_to_html(
time_format_locale,
},
bundle.unwrap_or(false),
Renderer::from_str(&renderer)?,
))?)
}

Expand Down
12 changes: 6 additions & 6 deletions vl-convert-python/tests/test_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@ def test_vegalite_to_html_no_bundle(name, vl_version):
html = vlc.vegalite_to_html(
vl_spec, vl_version=vl_version, bundle=False, theme="fivethirtyeight"
)
assert '{"theme":"fivethirtyeight"}' in html
assert '{"theme":"dark"}' not in html
assert '"theme":"fivethirtyeight"' in html
assert '"theme":"dark"' not in html

html = vlc.vegalite_to_html(
vl_spec, vl_version=vl_version, bundle=False, theme="dark"
)
assert '{"theme":"dark"}' in html
assert '"theme":"dark"' in html


@pytest.mark.parametrize("name", ["circle_binned"])
Expand Down Expand Up @@ -163,13 +163,13 @@ def test_vegalite_to_html_bundle(name, vl_version):
html = vlc.vegalite_to_html(
vl_spec, vl_version=vl_version, bundle=True, theme="fivethirtyeight"
)
assert '{"theme":"fivethirtyeight"}' in html
assert '{"theme":"dark"}' not in html
assert '"theme":"fivethirtyeight"' in html
assert '"theme":"dark"' not in html

html = vlc.vegalite_to_html(
vl_spec, vl_version=vl_version, bundle=True, theme="dark"
)
assert '{"theme":"dark"}' in html
assert '"theme":"dark"' in html


@pytest.mark.parametrize("name", ["circle_binned", "stacked_bar_h"])
Expand Down
53 changes: 49 additions & 4 deletions vl-convert-rs/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::module_loader::{
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter};
use std::io::Cursor;
use std::path::Path;
use std::rc::Rc;
Expand All @@ -24,6 +25,7 @@ use deno_runtime::worker::WorkerOptions;

use deno_runtime::deno_fs::RealFs;
use std::panic;
use std::str::FromStr;
use std::thread;
use std::thread::JoinHandle;

Expand Down Expand Up @@ -57,9 +59,14 @@ pub struct VgOpts {
}

impl VgOpts {
pub fn to_embed_opts(&self) -> Result<serde_json::Value, AnyError> {
pub fn to_embed_opts(&self, renderer: Renderer) -> Result<serde_json::Value, AnyError> {
let mut opts_map = serde_json::Map::new();

opts_map.insert(
"renderer".to_string(),
serde_json::Value::String(renderer.to_string()),
);

if let Some(format_locale) = &self.format_locale {
opts_map.insert("formatLocale".to_string(), format_locale.as_object()?);
}
Expand Down Expand Up @@ -114,6 +121,37 @@ impl TimeFormatLocale {
}
}

#[derive(Debug, Clone, Copy)]
pub enum Renderer {
Svg,
Canvas,
Hybrid,
}

impl Display for Renderer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let r = match self {
Renderer::Svg => "svg",
Renderer::Canvas => "canvas",
Renderer::Hybrid => "hybrid",
};
std::fmt::Display::fmt(r, f)
}
}

impl FromStr for Renderer {
type Err = AnyError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_ascii_lowercase().as_str() {
"svg" => Self::Svg,
"canvas" => Self::Canvas,
"hybrid" => Self::Hybrid,
_ => return Err(anyhow!("Unsupported renderer: {}", s)),
})
}
}

#[derive(Debug, Clone, Default)]
pub struct VlOpts {
pub config: Option<serde_json::Value>,
Expand All @@ -126,9 +164,14 @@ pub struct VlOpts {
}

impl VlOpts {
pub fn to_embed_opts(&self) -> Result<serde_json::Value, AnyError> {
pub fn to_embed_opts(&self, renderer: Renderer) -> Result<serde_json::Value, AnyError> {
let mut opts_map = serde_json::Map::new();

opts_map.insert(
"renderer".to_string(),
serde_json::Value::String(renderer.to_string()),
);

if let Some(theme) = &self.theme {
opts_map.insert(
"theme".to_string(),
Expand Down Expand Up @@ -1302,9 +1345,10 @@ impl VlConverter {
vl_spec: serde_json::Value,
vl_opts: VlOpts,
bundle: bool,
renderer: Renderer,
) -> Result<String, AnyError> {
let vl_version = vl_opts.vl_version;
let code = get_vega_or_vegalite_script(vl_spec, vl_opts.to_embed_opts()?)?;
let code = get_vega_or_vegalite_script(vl_spec, vl_opts.to_embed_opts(renderer)?)?;
self.build_html(&code, vl_version, bundle).await
}

Expand All @@ -1313,8 +1357,9 @@ impl VlConverter {
vg_spec: serde_json::Value,
vg_opts: VgOpts,
bundle: bool,
renderer: Renderer,
) -> Result<String, AnyError> {
let code = get_vega_or_vegalite_script(vg_spec, vg_opts.to_embed_opts()?)?;
let code = get_vega_or_vegalite_script(vg_spec, vg_opts.to_embed_opts(renderer)?)?;
self.build_html(&code, Default::default(), bundle).await
}

Expand Down
8 changes: 4 additions & 4 deletions vl-convert-rs/tests/test_specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ mod test_vegalite_to_vega {
mod test_vegalite_to_html_no_bundle {
use crate::*;
use futures::executor::block_on;
use vl_convert_rs::converter::VlOpts;
use vl_convert_rs::converter::{Renderer, VlOpts};
use vl_convert_rs::VlConverter;

#[rstest]
Expand Down Expand Up @@ -386,7 +386,7 @@ mod test_vegalite_to_html_no_bundle {
let mut converter = VlConverter::new();

let html_result = block_on(
converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, false)
converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, false, Renderer::Canvas)
).unwrap();

// Check for expected patterns
Expand All @@ -404,7 +404,7 @@ mod test_vegalite_to_html_no_bundle {
mod test_vegalite_to_html_bundle {
use crate::*;
use futures::executor::block_on;
use vl_convert_rs::converter::VlOpts;
use vl_convert_rs::converter::{Renderer, VlOpts};
use vl_convert_rs::VlConverter;

#[rstest]
Expand Down Expand Up @@ -434,7 +434,7 @@ mod test_vegalite_to_html_bundle {
let mut converter = VlConverter::new();

let html_result = block_on(
converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, true)
converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, true, Renderer::Svg)
).unwrap();

// Check for expected patterns
Expand Down
18 changes: 17 additions & 1 deletion vl-convert/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use itertools::Itertools;
use std::path::Path;
use std::str::FromStr;
use vl_convert_rs::converter::{
vega_to_url, vegalite_to_url, FormatLocale, TimeFormatLocale, VgOpts, VlConverter, VlOpts,
vega_to_url, vegalite_to_url, FormatLocale, Renderer, TimeFormatLocale, VgOpts, VlConverter,
VlOpts,
};
use vl_convert_rs::module_loader::import_map::VlVersion;
use vl_convert_rs::text::register_font_directory;
Expand Down Expand Up @@ -299,6 +300,10 @@ enum Commands {
/// d3-time-format locale name or file with .json extension
#[arg(long)]
time_format_locale: Option<String>,

/// Vega renderer. One of 'svg' (default), 'canvas', or 'hybrid'
#[arg(long)]
renderer: Option<String>,
},

/// Convert a Vega specification to an SVG image
Expand Down Expand Up @@ -468,6 +473,10 @@ enum Commands {
/// d3-time-format locale name or file with .json extension
#[arg(long)]
time_format_locale: Option<String>,

/// Vega renderer. One of 'svg' (default), 'canvas', or 'hybrid'
#[arg(long)]
renderer: Option<String>,
},

/// Convert an SVG image to a PNG image
Expand Down Expand Up @@ -702,6 +711,7 @@ async fn main() -> Result<(), anyhow::Error> {
bundle,
format_locale,
time_format_locale,
renderer,
} => {
// Initialize converter
let vl_str = read_input_string(&input)?;
Expand All @@ -717,6 +727,7 @@ async fn main() -> Result<(), anyhow::Error> {
None => None,
Some(p) => Some(time_format_locale_from_str(p)?),
};
let renderer = renderer.unwrap_or_else(|| "svg".to_string());

let mut converter = VlConverter::new();
let html = converter
Expand All @@ -732,6 +743,7 @@ async fn main() -> Result<(), anyhow::Error> {
time_format_locale,
},
bundle,
Renderer::from_str(&renderer)?,
)
.await?;
write_output_string(&output, &html)?;
Expand Down Expand Up @@ -829,6 +841,7 @@ async fn main() -> Result<(), anyhow::Error> {
bundle,
format_locale,
time_format_locale,
renderer,
} => {
// Initialize converter
let vg_str = read_input_string(&input)?;
Expand All @@ -844,6 +857,8 @@ async fn main() -> Result<(), anyhow::Error> {
Some(p) => Some(time_format_locale_from_str(p)?),
};

let renderer = renderer.unwrap_or_else(|| "svg".to_string());

let mut converter = VlConverter::new();
let html = converter
.vega_to_html(
Expand All @@ -854,6 +869,7 @@ async fn main() -> Result<(), anyhow::Error> {
time_format_locale,
},
bundle,
Renderer::from_str(&renderer)?,
)
.await?;
write_output_string(&output, &html)?;
Expand Down

0 comments on commit 4887a5b

Please sign in to comment.