Skip to content

Commit

Permalink
Svg/labels (metabase#17413)
Browse files Browse the repository at this point in the history
* Backend SVG rendering proof of concept [ci skip]

* Update cssbox to 5.0.0

* Render bar, line, and pie charts in js to svg

sparkline is now done in js, bar is now recognized and done in js, new
:categorical/donut as well

* Remove api route for render

* pass along render-type, not hardcoded to :inline

* Move bar chart above sparkline and remove line check

In order to introduce the bar chart type need it above the sparkline
check since it is otherwise the same except for display property of
the card. But lots of tests assume that this will get hit with a nil
display type set in testing so remove checking for `:line` allows all
the testing cases to hit the right type

* Fix tests now that bar graphs aren't html but images

* Include attachments for bar charts

* Move over to in-tree bundle

* Force everything [ci noskip]

trying to ensure that the built jar includes the newer
"resources/frontend_client/app/dist/lib-static-viz.bundle.js"

* Run `yarn build-static-viz` in backend-deps in CI

this js file is now a hard dependency of the backend so it fits in
this tsk. All such things that depend on the backend sources will need
it. Makes me think perhaps we want a checked in version but i'm not
sure yet.

* Look on classpath not filesystem for js bundle [ci noskip]

* Move yarn build-static-viz into the checkout step

* License information for antlr4-runtime

* create attachment for categorical donuts

* add ordinal legend to donuts (metabase#17177)

* set widths of html image and svg image to 1200

* Revert "add ordinal legend to donuts (metabase#17177)"

This reverts commit 1eb81d2.

* Helper functions to render html easily

* readme in dev

* readme ensure that static viz bundle exists

* Cleanup ns after removing proxy

* Donut chart colors and legend (metabase#17251)

* use external color map for fill per dimension

* Add support new color legend for donut

* Ensure text doesn't appear as link

entire thing is actually the body of a link tag for emails but we want
a decent text color rather than a default link color

* use chart colors from https://stats.metabase.com/_internal/colors

* Make checkers happy

- remove unused imports
- add a docstring
- don't shadow fn with a local

* cleanup ns import

* Remove reflective call

* Cleanup ns on correct branch

Co-authored-by: dan sutton <[email protected]>

* X-axis: just use (approx) 5 ticks to avoid overlapping labels (metabase#17287)

* increase gap between arcs (metabase#17271)

* Set rendering hints on html->image

* ignore width for now and make them larger

* Ns deprecation and some cleanup

* make namespace checker happy

* Simple tests for detecting chart type

* Rename from poc

* Tests for scalar/smartscalar

* cleanup js svg namespace a bit

* Tests of svg engine

* ns sorting after renaming

* Unify our two different js engine usages

settled on the js context. Has typed returns `(.asString ^Value ...)`
instead of perhaps capturing std out?
https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html

Context is a bit more friendly for getting source into it. One
downside is that the invocable bit isn't quite as nice. The old way
would return a java.util.functionFunction but the difference is

(.apply function (object-array args))

vs

(.execute fn-ref (object-array args))

* Don't io/resource the io/resource

* js engine tests

* Ns cleanup in js-svg

type hints in the js-engine ns mean we don't need as many classes from
polyglot here

* add custom labels

* clean up margin

* Include labels in backend

* alignment

Co-authored-by: Cam Saul <[email protected]>
Co-authored-by: dan sutton <[email protected]>
Co-authored-by: Ariya Hidayat <[email protected]>
  • Loading branch information
4 people authored Aug 17, 2021
1 parent efce021 commit b8c9302
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 45 deletions.
6 changes: 6 additions & 0 deletions frontend/src/metabase/internal/pages/StaticVizPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default function StaticVizPage() {
x: row => new Date(row[0]).valueOf(),
y: row => row[1],
},
labels: {
bottom: "Created At",
},
}),
}}
></Box>
Expand All @@ -47,6 +50,9 @@ export default function StaticVizPage() {
x: row => new Date(row[0]).valueOf(),
y: row => row[1],
},
labels: {
left: "Count",
},
}),
}}
></Box>
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/metabase/static-viz/timeseries/bar.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import { Bar } from "@visx/shape";
import { AxisLeft, AxisBottom } from "@visx/axis";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { bottomAxisTickStyles, leftAxisTickStyles } from "../utils.js";
import { GridRows } from "@visx/grid";

export default function TimeseriesBar(
{ data, yScaleType = scaleLinear, accessors },
{ data, yScaleType = scaleLinear, accessors, labels },
layout,
) {
const leftMargin = 55;
let multiScale, categories;
const xAxisScale = scaleBand({
domain: data.map(accessors.x),
range: [40, layout.xMax],
range: [leftMargin, layout.xMax],
round: true,
padding: 0.2,
});
Expand All @@ -37,8 +39,8 @@ export default function TimeseriesBar(
<svg width={layout.width} height={layout.height}>
<GridRows
scale={yAxisScale}
width={layout.width}
left={40}
width={layout.xMax - leftMargin}
left={leftMargin}
strokeDasharray="4"
/>
{data.map(d => {
Expand All @@ -64,8 +66,8 @@ export default function TimeseriesBar(
return String(d);
}}
scale={yAxisScale}
label={"Count"}
left={40}
label={labels.left || t`Count`}
left={leftMargin}
tickLabelProps={() => leftAxisTickStyles(layout)}
/>
<AxisBottom
Expand All @@ -75,7 +77,7 @@ export default function TimeseriesBar(
tickFormat={d => new Date(d).toLocaleDateString("en")}
scale={xAxisScale}
stroke={layout.colors.axis.stroke}
label={"Time"}
label={labels.bottom || t`Time`}
tickLabelProps={() => bottomAxisTickStyles(layout)}
/>
</svg>
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/metabase/static-viz/timeseries/line.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import { LinePath } from "@visx/shape";
import { AxisLeft, AxisBottom } from "@visx/axis";
import { scaleLinear, scaleOrdinal, scaleTime } from "@visx/scale";
import { bottomAxisTickStyles, leftAxisTickStyles } from "../utils";
import { GridRows } from "@visx/grid";

export default function TimeseriesLine(
{ data, yScaleType = scaleLinear, accessors },
{ data, yScaleType = scaleLinear, accessors, labels },
layout,
) {
const leftMargin = 55;
let multiScale, categories;

const xAxisScale = scaleTime({
domain: [
Math.min(...data.map(accessors.x)),
Math.max(...data.map(accessors.x)),
],
range: [40, layout.xMax],
range: [leftMargin, layout.xMax],
});

// Y scale
Expand All @@ -39,8 +41,8 @@ export default function TimeseriesLine(
<svg width={layout.width} height={layout.height}>
<GridRows
scale={yAxisScale}
width={layout.width}
left={40}
width={layout.xMax - leftMargin}
left={leftMargin}
strokeDasharray="4"
/>
{multiScale ? (
Expand All @@ -67,16 +69,16 @@ export default function TimeseriesLine(
/>
)}
<AxisLeft
label={"Count"}
label={labels.left || t`Metric`}
hideTicks
hideAxisLine
tickFormat={d => String(d)}
scale={yAxisScale}
left={40}
left={leftMargin}
tickLabelProps={() => leftAxisTickStyles(layout)}
/>
<AxisBottom
label={"Time"}
label={labels.bottom || t`Dimension`}
hideTicks={false}
numTicks={5}
top={layout.yMax}
Expand Down
28 changes: 17 additions & 11 deletions src/metabase/pulse/render/body.clj
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,15 @@
(list table-body))}))

(s/defmethod render :bar :- common/RenderedPulseCard
[_ render-type _timezone-id :- (s/maybe s/Str) card {:keys [_cols] :as data}]
[_ render-type _timezone-id :- (s/maybe s/Str) card {:keys [cols] :as data}]
(let [[x-axis-rowfn y-axis-rowfn] (common/graphing-column-row-fns card data)
rows (common/non-nil-rows x-axis-rowfn y-axis-rowfn (:rows data))
image-bundle (image-bundle/make-image-bundle
render-type
(js-svg/timelineseries-bar
(mapv (juxt x-axis-rowfn y-axis-rowfn) rows)))]
render-type
(js-svg/timelineseries-bar
(mapv (juxt x-axis-rowfn y-axis-rowfn) rows)
{:bottom (-> cols x-axis-rowfn :display_name)
:left (-> cols y-axis-rowfn :display_name)}))]
{:attachments
(when image-bundle
(image-bundle/image-bundle->attachment image-bundle))
Expand All @@ -243,10 +245,10 @@
rows (common/non-nil-rows x-axis-rowfn y-axis-rowfn rows)
legend-colors (zipmap (map (comp str x-axis-rowfn) rows) (cycle colors))
image-bundle (image-bundle/make-image-bundle
render-type
(js-svg/categorical-donut
(mapv (juxt x-axis-rowfn y-axis-rowfn) rows)
legend-colors))]
render-type
(js-svg/categorical-donut
(mapv (juxt x-axis-rowfn y-axis-rowfn) rows)
legend-colors))]
{:attachments
(when image-bundle
(image-bundle/image-bundle->attachment image-bundle))
Expand Down Expand Up @@ -311,17 +313,21 @@
@error-rendered-info))))

(s/defmethod render :sparkline :- common/RenderedPulseCard
[_ render-type timezone-id card {:keys [rows cols] :as data}]
[_ render-type timezone-id card {:keys [_rows cols] :as data}]
(let [[x-axis-rowfn
y-axis-rowfn] (common/graphing-column-row-fns card data)
rows (sparkline/cleaned-rows timezone-id card data)
last-rows (reverse (take-last 2 rows))
values (for [row last-rows]
(some-> row y-axis-rowfn common/format-number))
labels (datetime/format-temporal-string-pair timezone-id (map x-axis-rowfn last-rows) (x-axis-rowfn cols))
labels (datetime/format-temporal-string-pair timezone-id
(map x-axis-rowfn last-rows)
(x-axis-rowfn cols))
image-bundle (image-bundle/make-image-bundle
render-type
(js-svg/timelineseries-line (mapv (juxt x-axis-rowfn y-axis-rowfn) rows)))]
(js-svg/timelineseries-line (mapv (juxt x-axis-rowfn y-axis-rowfn) rows)
{:bottom (-> cols x-axis-rowfn :display_name)
:left (-> cols y-axis-rowfn :display_name)}))]
{:attachments
(when image-bundle
(image-bundle/image-bundle->attachment image-bundle))
Expand Down
24 changes: 14 additions & 10 deletions src/metabase/pulse/render/js_svg.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,18 @@ const dimension_accessors = {
metric: (row) => row[1],
}
function timeseries_line (data) {
function timeseries_line (data, labels) {
return StaticViz.RenderChart(\"timeseries/line\", {
data: toJSArray(data),
labels: toJSMap(labels),
accessors: date_accessors
})
}
function timeseries_bar (data) {
function timeseries_bar (data, labels) {
return StaticViz.RenderChart(\"timeseries/bar\", {
data: toJSArray(data),
labels: toJSMap(labels),
accessors: date_accessors
})
}
Expand Down Expand Up @@ -146,17 +148,19 @@ function categorical_donut (rows, colors) {
(-> s parse-svg-string render-svg))

(defn timelineseries-line
"Clojure entrypoint to render a timeseries line char. Rows should be tuples of [datetime numeric-value]. Returns a
byte array of a png file."
[rows]
(let [svg-string (.asString (js/execute-fn-name @context "timeseries_line" rows))]
"Clojure entrypoint to render a timeseries line char. Rows should be tuples of [datetime numeric-value]. Labels is a
map of {:left \"left-label\" :right \"right-label\"}. Returns a byte array of a png file."
[rows labels]
(let [svg-string (.asString (js/execute-fn-name @context "timeseries_line" rows
(map (fn [[k v]] [(name k) v]) labels)))]
(svg-string->bytes svg-string)))

(defn timelineseries-bar
"Clojure entrypoint to render a timeseries bar char. Rows should be tuples of [datetime numeric-value]. Returns a byte
array of a png file"
[rows]
(let [svg-string (.asString (js/execute-fn-name @context "timeseries_bar" rows))]
"Clojure entrypoint to render a timeseries bar char. Rows should be tuples of [datetime numeric-value]. Labels is a
map of {:left \"left-label\" :right \"right-label\"}. Returns a byte array of a png file"
[rows labels]
(let [svg-string (.asString (js/execute-fn-name @context "timeseries_bar" rows
(map (fn [[k v]] [(name k) v]) labels)))]
(svg-string->bytes svg-string)))

(defn categorical-donut
Expand Down
20 changes: 10 additions & 10 deletions test/metabase/pulse/render/js_svg_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
(fn [^Node node] (swap! nodes conj (.getNodeName node))))
(is (= ["svg" "g" "line" "g" "rect" "g" "circle"] @nodes))))

(binding [*test-out* *out*] (post-process-test))

(deftest fix-fill-test
(let [svg "<svg ><line x1=\"0\" y1=\"260\" x2=\"540\" y2=\"260\" fill=\"transparent\"></line></svg>"

Expand Down Expand Up @@ -70,21 +68,23 @@
(set/intersection #{"div" "span" "p"}))))))

(deftest timelineseries-line-test
(let [rows [[#t "2020" 2]
[#t "2021" 3]]]
(let [rows [[#t "2020" 2]
[#t "2021" 3]]
labels {:left "count" :bottom "year"}]
(testing "It returns bytes"
(let [svg-bytes (js-svg/timelineseries-line rows)]
(let [svg-bytes (js-svg/timelineseries-line rows labels)]
(is (bytes? svg-bytes))))
(let [svg-string (.asString ^Value (js/execute-fn-name @context "timeseries_line" rows))]
(let [svg-string (.asString ^Value (js/execute-fn-name @context "timeseries_line" rows labels))]
(validate-svg-string :timelineseries-line svg-string))))

(deftest timelineseries-bar-test
(let [rows [[#t "2020" 2]
[#t "2021" 3]]]
(let [rows [[#t "2020" 2]
[#t "2021" 3]]
labels {:left "count" :bottom "year"}]
(testing "It returns bytes"
(let [svg-bytes (js-svg/timelineseries-bar rows)]
(let [svg-bytes (js-svg/timelineseries-bar rows labels)]
(is (bytes? svg-bytes))))
(let [svg-string (.asString ^Value (js/execute-fn-name @context "timeseries_bar" rows))]
(let [svg-string (.asString ^Value (js/execute-fn-name @context "timeseries_bar" rows labels))]
(validate-svg-string :timelineseries-bar svg-string))))

(deftest categorical-donut-test
Expand Down

0 comments on commit b8c9302

Please sign in to comment.