diff --git a/gen-test-data/vega-specs/text/bar_axis_labels.vg.json b/gen-test-data/vega-specs/text/bar_axis_labels.vg.json new file mode 100644 index 0000000..e3cf559 --- /dev/null +++ b/gen-test-data/vega-specs/text/bar_axis_labels.vg.json @@ -0,0 +1,135 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "A bar graph showing what activities consume what percentage of the day.", + "background": "white", + "padding": 5, + "width": 200, + "style": "cell", + "config": {"style": {"cell": {"stroke": "transparent"}}}, + "data": [ + { + "name": "source_0", + "values": [ + {"Activity": "Sleeping", "Time": 8}, + {"Activity": "Eating", "Time": 2}, + {"Activity": "TV", "Time": 4}, + {"Activity": "Work", "Time": 8}, + {"Activity": "Exercise", "Time": 2} + ] + }, + { + "name": "data_0", + "source": "source_0", + "transform": [ + { + "type": "joinaggregate", + "as": ["TotalTime"], + "ops": ["sum"], + "fields": ["Time"] + }, + { + "type": "formula", + "expr": "datum.Time/datum.TotalTime * 100", + "as": "PercentOfTotal" + }, + { + "type": "stack", + "groupby": ["Activity"], + "field": "PercentOfTotal", + "sort": {"field": [], "order": []}, + "as": ["PercentOfTotal_start", "PercentOfTotal_end"], + "offset": "zero" + }, + { + "type": "filter", + "expr": "isValid(datum[\"PercentOfTotal\"]) && isFinite(+datum[\"PercentOfTotal\"])" + } + ] + } + ], + "signals": [ + {"name": "y_step", "value": 12}, + { + "name": "height", + "update": "bandspace(domain('y').length, 0.1, 0.05) * y_step" + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_0"}, + "encode": { + "update": { + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"% of total Time: \" + (format(datum[\"PercentOfTotal\"], \"\")) + \"; Activity: \" + (isValid(datum[\"Activity\"]) ? datum[\"Activity\"] : \"\"+datum[\"Activity\"])" + }, + "x": {"scale": "x", "field": "PercentOfTotal_end"}, + "x2": {"scale": "x", "field": "PercentOfTotal_start"}, + "y": {"scale": "y", "field": "Activity"}, + "height": {"signal": "max(0.25, bandwidth('y'))"} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "domain": { + "data": "data_0", + "fields": ["PercentOfTotal_start", "PercentOfTotal_end"] + }, + "range": [0, {"signal": "width"}], + "nice": true, + "zero": true + }, + { + "name": "y", + "type": "band", + "domain": {"data": "data_0", "field": "Activity", "sort": true}, + "range": {"step": {"signal": "y_step"}}, + "paddingInner": 0.1, + "paddingOuter": 0.05 + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "% of total Time", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "labelFont": "Helvetica", + "titleFont": "Helvetica", + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "labelFont": "Helvetica", + "titleFont": "Helvetica", + "zindex": 0 + } + ] +} diff --git a/vega-wgpu-renderer/src/renderers/text.rs b/vega-wgpu-renderer/src/renderers/text.rs index 2bf07fe..c33f1ed 100644 --- a/vega-wgpu-renderer/src/renderers/text.rs +++ b/vega-wgpu-renderer/src/renderers/text.rs @@ -1,11 +1,11 @@ use crate::renderers::canvas::CanvasUniform; use crate::renderers::mark::MarkShader; use crate::scene::text::TextInstance; -use crate::specs::text::{TextAlignSpec, TextBaselineSpec}; +use crate::specs::text::{FontWeightNameSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec}; use glyphon::cosmic_text::Align; use glyphon::{ Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, - TextAtlas, TextBounds, TextRenderer, + TextAtlas, TextBounds, TextRenderer, Weight, }; use wgpu::{ CommandBuffer, CommandEncoderDescriptor, Device, MultisampleState, Operations, Queue, @@ -69,12 +69,26 @@ impl TextMarkRenderer { .map(|instance| { let mut buffer = Buffer::new( &mut self.font_system, - Metrics::new(instance.font_size, instance.font_size * 1.0), + Metrics::new(instance.font_size, instance.font_size), ); + let family = match instance.font.to_lowercase().as_str() { + "serif" => Family::Serif, + "sans serif" => Family::SansSerif, + "cursive" => Family::Cursive, + "fantasy" => Family::Fantasy, + "monospace" => Family::Monospace, + _ => Family::Name(instance.font.as_str()), + }; + let weight = match instance.font_weight { + FontWeightSpec::Name(FontWeightNameSpec::Bold) => Weight::BOLD, + FontWeightSpec::Name(FontWeightNameSpec::Normal) => Weight::NORMAL, + FontWeightSpec::Number(w) => Weight(w as u16), + }; + buffer.set_text( &mut self.font_system, &instance.text, - Attrs::new().family(Family::SansSerif), + Attrs::new().family(family).weight(weight), Shaping::Advanced, ); buffer.set_size( @@ -102,8 +116,9 @@ impl TextMarkRenderer { let top = match instance.baseline { TextBaselineSpec::Alphabetic => instance.position[1] - height, - TextBaselineSpec::Top => instance.position[1], - TextBaselineSpec::Middle => instance.position[1] - height * 0.56, + // Add half pixel for top baseline for better match with resvg + TextBaselineSpec::Top => instance.position[1] + 0.5, + TextBaselineSpec::Middle => instance.position[1] - height * 0.5, TextBaselineSpec::Bottom => instance.position[1] - height, TextBaselineSpec::LineTop => todo!(), TextBaselineSpec::LineBottom => todo!(), diff --git a/vega-wgpu-renderer/src/scene/text.rs b/vega-wgpu-renderer/src/scene/text.rs index eb3856f..db77b6c 100644 --- a/vega-wgpu-renderer/src/scene/text.rs +++ b/vega-wgpu-renderer/src/scene/text.rs @@ -63,8 +63,8 @@ impl TextInstance { font: item_spec .font .clone() - .unwrap_or_else(|| "Liberation Sans".to_string()), - font_size: item_spec.fill_opacity.unwrap_or(12.0), + .unwrap_or_else(|| "Sans Serif".to_string()), + font_size: item_spec.font_size.unwrap_or(10.0), font_weight: item_spec.font_weight.unwrap_or_default(), font_style: item_spec.font_style.unwrap_or_default(), limit: item_spec.limit.unwrap_or(0.0), diff --git a/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.dims.json b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.dims.json new file mode 100644 index 0000000..25e83d0 --- /dev/null +++ b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.dims.json @@ -0,0 +1,6 @@ +{ + "width": 257, + "height": 102, + "origin_x": 51, + "origin_y": 5 +} \ No newline at end of file diff --git a/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.png b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.png new file mode 100644 index 0000000..bda8635 Binary files /dev/null and b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.png differ diff --git a/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.sg.json b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.sg.json new file mode 100644 index 0000000..9556005 --- /dev/null +++ b/vega-wgpu-renderer/tests/specs/text/bar_axis_labels.sg.json @@ -0,0 +1,567 @@ +{ + "marktype": "group", + "name": "root", + "role": "frame", + "interactive": true, + "clip": false, + "items": [ + { + "items": [ + { + "marktype": "group", + "role": "axis", + "interactive": false, + "clip": false, + "items": [ + { + "items": [ + { + "marktype": "rule", + "role": "axis-grid", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 29, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 57, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 86, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 114, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 143, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 171, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + }, + { + "x": 200, + "y": 0, + "opacity": 1, + "stroke": "#ddd", + "strokeWidth": 1, + "y2": -60 + } + ], + "zindex": 0 + } + ], + "x": 0.5, + "y": 60.5, + "orient": "bottom" + } + ], + "zindex": 0, + "aria": false + }, + { + "marktype": "group", + "role": "axis", + "interactive": false, + "clip": false, + "items": [ + { + "items": [ + { + "marktype": "rule", + "role": "axis-tick", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 29, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 57, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 86, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 114, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 143, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 171, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + }, + { + "x": 200, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 5 + } + ], + "zindex": 0 + }, + { + "marktype": "text", + "role": "axis-label", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 7, + "align": "left", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "0", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 28.57142857142857, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "5", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 57.14285714285714, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "10", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 85.71428571428571, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "15", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 114.28571428571428, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "20", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 142.85714285714286, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "25", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 171.42857142857142, + "y": 7, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "30", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": 200, + "y": 7, + "align": "right", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "35", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + } + ], + "zindex": 0 + }, + { + "marktype": "rule", + "role": "axis-domain", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": 200 + } + ], + "zindex": 0 + }, + { + "marktype": "text", + "role": "axis-title", + "interactive": false, + "clip": false, + "items": [ + { + "x": 100, + "y": 21, + "align": "center", + "baseline": "top", + "fill": "#000", + "opacity": 1, + "text": "% of total Time", + "angle": 0, + "font": "Helvetica", + "fontSize": 11, + "fontWeight": "bold" + } + ], + "zindex": 0 + } + ], + "x": 0.5, + "y": 60.5, + "orient": "bottom" + } + ], + "zindex": 0 + }, + { + "marktype": "group", + "role": "axis", + "interactive": false, + "clip": false, + "items": [ + { + "items": [ + { + "marktype": "rule", + "role": "axis-tick", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 5, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": -5 + }, + { + "x": 0, + "y": 18, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": -5 + }, + { + "x": 0, + "y": 30, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": -5 + }, + { + "x": 0, + "y": 41, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": -5 + }, + { + "x": 0, + "y": 53, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "x2": -5 + } + ], + "zindex": 0 + }, + { + "marktype": "text", + "role": "axis-label", + "interactive": false, + "clip": false, + "items": [ + { + "x": -7, + "y": 5.499999999999998, + "align": "right", + "baseline": "middle", + "fill": "#000", + "opacity": 1, + "text": "Eating", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": -7, + "y": 17.5, + "align": "right", + "baseline": "middle", + "fill": "#000", + "opacity": 1, + "text": "Exercise", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": -7, + "y": 29.5, + "align": "right", + "baseline": "middle", + "fill": "#000", + "opacity": 1, + "text": "Sleeping", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": -7, + "y": 41.49999999999999, + "align": "right", + "baseline": "middle", + "fill": "#000", + "opacity": 1, + "text": "TV", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + }, + { + "x": -7, + "y": 53.49999999999999, + "align": "right", + "baseline": "middle", + "fill": "#000", + "opacity": 1, + "text": "Work", + "angle": 0, + "limit": 180, + "font": "Helvetica", + "fontSize": 10 + } + ], + "zindex": 0 + }, + { + "marktype": "rule", + "role": "axis-domain", + "interactive": false, + "clip": false, + "items": [ + { + "x": 0, + "y": 0, + "opacity": 1, + "stroke": "#888", + "strokeWidth": 1, + "y2": 60 + } + ], + "zindex": 0 + } + ], + "x": 0.5, + "y": 0.5, + "orient": "left" + } + ], + "zindex": 0 + }, + { + "marktype": "rect", + "name": "marks", + "role": "mark", + "interactive": true, + "clip": false, + "items": [ + { + "x": 0, + "y": 24.599999999999998, + "width": 190.47619047619045, + "height": 10.8, + "fill": "#4c78a8", + "x2": 190.47619047619045, + "description": "% of total Time: 33.3333333333; Activity: Sleeping", + "ariaRoleDescription": "bar" + }, + { + "x": 0, + "y": 0.5999999999999979, + "width": 47.61904761904761, + "height": 10.8, + "fill": "#4c78a8", + "x2": 47.61904761904761, + "description": "% of total Time: 8.33333333333; Activity: Eating", + "ariaRoleDescription": "bar" + }, + { + "x": 0, + "y": 36.599999999999994, + "width": 95.23809523809523, + "height": 10.8, + "fill": "#4c78a8", + "x2": 95.23809523809523, + "description": "% of total Time: 16.6666666667; Activity: TV", + "ariaRoleDescription": "bar" + }, + { + "x": 0, + "y": 48.599999999999994, + "width": 190.47619047619045, + "height": 10.8, + "fill": "#4c78a8", + "x2": 190.47619047619045, + "description": "% of total Time: 33.3333333333; Activity: Work", + "ariaRoleDescription": "bar" + }, + { + "x": 0, + "y": 12.599999999999998, + "width": 47.61904761904761, + "height": 10.8, + "fill": "#4c78a8", + "x2": 47.61904761904761, + "description": "% of total Time: 8.33333333333; Activity: Exercise", + "ariaRoleDescription": "bar" + } + ], + "zindex": 0 + } + ], + "x": 0, + "y": 0, + "width": 200, + "height": 60, + "fill": "transparent", + "stroke": "transparent" + } + ], + "zindex": 0 +} \ No newline at end of file diff --git a/vega-wgpu-renderer/tests/test_image_baselines.rs b/vega-wgpu-renderer/tests/test_image_baselines.rs index cb5d239..48b8635 100644 --- a/vega-wgpu-renderer/tests/test_image_baselines.rs +++ b/vega-wgpu-renderer/tests/test_image_baselines.rs @@ -28,7 +28,8 @@ mod test_image_baselines { case("symbol", "binned_scatter_arrow", 0.001), case("symbol", "binned_scatter_cross", 0.001), case("symbol", "binned_scatter_circle", 0.001), - case("rule", "wide_rule_axes", 0.0001) + case("rule", "wide_rule_axes", 0.0001), + case("text", "bar_axis_labels", 0.01) )] fn test_image_baseline(category: &str, spec_name: &str, tolerance: f64) { let specs_dir = format!("{}/tests/specs/{category}", env!("CARGO_MANIFEST_DIR"));