From 09de384805564a5427e23c19eb11153d14eba176 Mon Sep 17 00:00:00 2001 From: Paul Rosenzweig <paulrosenzweig@users.noreply.github.com> Date: Wed, 28 Aug 2019 11:49:01 -0400 Subject: [PATCH] Convert nulls to "(empty)" in row charts and line area bar charts (#10609) --- .../visualizations/lib/RowRenderer.js | 15 ++++--- .../metabase/visualizations/lib/constants.js | 3 ++ .../visualizations/lib/renderer_utils.js | 7 ++- .../LineAreaBarRenderer-bar.unit.spec.js | 28 ++++++++++-- .../components/RowRenderer.unit.spec.js | 43 +++++++++++++++++++ 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 frontend/src/metabase/visualizations/lib/constants.js create mode 100644 frontend/test/metabase/visualizations/components/RowRenderer.unit.spec.js diff --git a/frontend/src/metabase/visualizations/lib/RowRenderer.js b/frontend/src/metabase/visualizations/lib/RowRenderer.js index 62300e5327c..e9d9e954213 100644 --- a/frontend/src/metabase/visualizations/lib/RowRenderer.js +++ b/frontend/src/metabase/visualizations/lib/RowRenderer.js @@ -9,6 +9,7 @@ import { formatValue } from "metabase/lib/formatting"; import { initChart, forceSortedGroup, makeIndexMap } from "./renderer_utils"; import { getFriendlyName } from "./utils"; import { checkXAxisLabelOverlap } from "./LineAreaBarPostRender"; +import { NULL_DISPLAY_VALUE } from "./constants"; export default function rowRenderer( element, @@ -25,16 +26,20 @@ export default function rowRenderer( // disable clicks chart.onClick = () => {}; - const formatDimension = row => - formatValue(row[0], { column: cols[0], type: "axis" }); - // dc.js doesn't give us a way to format the row labels from unformatted data, so we have to // do it here then construct a mapping to get the original dimension for tooltipsd/clicks - const rows = series[0].data.rows.map(row => [formatDimension(row), row[1]]); + const rowsWithFormattedNull = series[0].data.rows.map(([first, ...rest]) => [ + first === null ? NULL_DISPLAY_VALUE : first, + ...rest, + ]); + const rows = rowsWithFormattedNull.map(([a, b]) => [ + formatValue(a, { column: cols[0], type: "axis" }), + b, + ]); const formattedDimensionMap = new Map( rows.map(([formattedDimension], index) => [ formattedDimension, - series[0].data.rows[index][0], + rowsWithFormattedNull[index][0], ]), ); diff --git a/frontend/src/metabase/visualizations/lib/constants.js b/frontend/src/metabase/visualizations/lib/constants.js new file mode 100644 index 00000000000..1a96249d380 --- /dev/null +++ b/frontend/src/metabase/visualizations/lib/constants.js @@ -0,0 +1,3 @@ +import { t } from "ttag"; + +export const NULL_DISPLAY_VALUE = t`(empty)`; diff --git a/frontend/src/metabase/visualizations/lib/renderer_utils.js b/frontend/src/metabase/visualizations/lib/renderer_utils.js index 54d11e8c065..739e2a7792f 100644 --- a/frontend/src/metabase/visualizations/lib/renderer_utils.js +++ b/frontend/src/metabase/visualizations/lib/renderer_utils.js @@ -10,6 +10,7 @@ import { dimensionIsNumeric } from "./numeric"; import { dimensionIsTimeseries } from "./timeseries"; import { getAvailableCanvasWidth, getAvailableCanvasHeight } from "./utils"; import { invalidDateWarning, nullDimensionWarning } from "./warnings"; +import { NULL_DISPLAY_VALUE } from "./constants"; export function initChart(chart, element) { // set the bounds @@ -109,7 +110,11 @@ const memoizedParseXValue = _.memoize( if (isTimeseries && !isQuantitative) { return parseTimestampAndWarn(xValue, unit); } - const parsedValue = isNumeric ? xValue : String(xValue); + const parsedValue = isNumeric + ? xValue + : xValue === null + ? NULL_DISPLAY_VALUE + : String(xValue); return { parsedValue }; }, // create cache key from args diff --git a/frontend/test/metabase/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js b/frontend/test/metabase/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js index e4659a96330..dfa6d4c345b 100644 --- a/frontend/test/metabase/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js +++ b/frontend/test/metabase/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js @@ -27,7 +27,7 @@ const DEFAULT_COLUMN_SETTINGS = { date_style: "MMMM D, YYYY", }; -function MainSeries(chartType, settings = {}, value = 1) { +function MainSeries(chartType, settings = {}, { key = "A", value = 1 } = {}) { return { card: { display: chartType, @@ -41,7 +41,7 @@ function MainSeries(chartType, settings = {}, value = 1) { StringColumn({ display_name: "Category", source: "breakout" }), NumberColumn({ display_name: "Sum", source: "aggregation" }), ], - rows: [["A", value]], + rows: [[key, value]], }, }; } @@ -184,7 +184,13 @@ describe("LineAreaBarRenderer-bar", () => { const onHoverChange = jest.fn(); renderLineAreaBar( element, - [MainSeries("bar", { "stackable.stack_type": "normalized" }, 3)], + [ + MainSeries( + "bar", + { "stackable.stack_type": "normalized" }, + { value: 3 }, + ), + ], { onHoverChange }, ); @@ -218,6 +224,22 @@ describe("LineAreaBarRenderer-bar", () => { { key: "Foo", value: 1 }, ]); }); + + it('should render "(empty)" for nulls', () => { + const onHoverChange = jest.fn(); + renderLineAreaBar(element, [MainSeries("bar", {}, { key: null })], { + onHoverChange, + }); + + dispatchUIEvent(qsa(".bar, .dot")[0], "mousemove"); + + const { calls } = onHoverChange.mock; + const [{ value }] = getDataKeyValues(calls[0][0]); + expect(value).toEqual("(empty)"); + + const tick = element.querySelector(".axis.x .tick text"); + expect(tick.textContent).toEqual("(empty)"); + }); }); function getDataKeyValues(hover) { diff --git a/frontend/test/metabase/visualizations/components/RowRenderer.unit.spec.js b/frontend/test/metabase/visualizations/components/RowRenderer.unit.spec.js new file mode 100644 index 00000000000..d82d1ec925e --- /dev/null +++ b/frontend/test/metabase/visualizations/components/RowRenderer.unit.spec.js @@ -0,0 +1,43 @@ +import "__support__/mocks"; + +import { + createFixture, + cleanupFixture, + renderChart, + NumberColumn, + StringColumn, +} from "../__support__/visualizations"; + +import rowRenderer from "metabase/visualizations/lib/RowRenderer"; + +describe("RowChart", () => { + let element; + + beforeEach(function() { + element = createFixture(); + }); + + afterEach(function() { + cleanupFixture(element); + }); + + it("should render", () => { + renderChart(rowRenderer, element, [ + { + card: { display: "row", visualization_settings: {} }, + data: { cols: [StringColumn(), NumberColumn()], rows: [["a", 1]] }, + }, + ]); + }); + + it('should render null as "(empty)"', () => { + renderChart(rowRenderer, element, [ + { + card: { display: "row", visualization_settings: {} }, + data: { cols: [StringColumn(), NumberColumn()], rows: [[null, 1]] }, + }, + ]); + + expect(element.querySelector("text.row").innerHTML).toBe("(empty)"); + }); +}); -- GitLab