diff --git a/.circleci/config.yml b/.circleci/config.yml index 44bcd47363..3f6d8e5b0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,7 @@ jobs: - run: sudo pip3 install -r requirements_bundles.txt - run: npm ci - run: npm run bundle - - run: npm test + - run: npm test -- --runInBand - run: npm run lint frontend-e2e-tests: environment: diff --git a/client/app/visualizations/counter/Editor/CounterValueOptions.jsx b/client/app/visualizations/counter/Editor/CounterValueOptions.jsx new file mode 100644 index 0000000000..fe8ee7cb12 --- /dev/null +++ b/client/app/visualizations/counter/Editor/CounterValueOptions.jsx @@ -0,0 +1,142 @@ +import { isNil, get, map, includes } from "lodash"; +import React from "react"; +import PropTypes from "prop-types"; +import { Section, InputNumber, Input, Select, Checkbox, ContextHelp } from "@/components/visualizations/editor"; + +export default function CounterValueOptions({ disabled, counterTypes, options, data, onChange }) { + const additionalOptions = get(counterTypes, [options.type, "options"], []); + const canReturnRow = get(counterTypes, [options.type, "canReturnRow"], false); + + const formatInfo = ( + +
Use special names to access additional properties:
+
+ {"{{ @@value }}"} raw value (as string); +
+
+ {"{{ @@value_formatted }}"} formatted value; +
+ {canReturnRow && ( +
+ Query result columns can be referenced using {"{{ column_name }}"} syntax. +
+ )} +
+ ); + + return ( + +
+ +
+ + {includes(additionalOptions, "column") && ( +
+ +
+ )} + + {includes(additionalOptions, "rowNumber") && ( +
+ onChange({ rowNumber })} + /> +
+ )} + +
+ Display Format {formatInfo}} + data-test="Counter.DisplayFormat" + className="w-100" + disabled={disabled} + defaultValue={options.displayFormat} + onChange={e => onChange({ displayFormat: e.target.value })} + /> +
+ +
+ onChange({ showTooltip: e.target.checked })}> + Show Tooltip + +
+ +
+ Tooltip Format {formatInfo}} + data-test="Counter.TooltipFormat" + className="w-100" + disabled={disabled || !options.showTooltip} + defaultValue={options.tooltipFormat} + onChange={e => onChange({ tooltipFormat: e.target.value })} + /> +
+
+ ); +} + +CounterValueOptions.propTypes = { + disabled: PropTypes.bool, + counterTypes: PropTypes.object, + options: PropTypes.shape({ + type: PropTypes.string, + column: PropTypes.string, + rowNumber: PropTypes.number, + displayFormat: PropTypes.string, + showTooltip: PropTypes.bool, + tooltipFormat: PropTypes.string, + }).isRequired, + data: PropTypes.shape({ + columns: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + }).isRequired + ), + }).isRequired, + onChange: PropTypes.func, +}; + +CounterValueOptions.defaultProps = { + disabled: false, + counterTypes: {}, + onChange: () => {}, +}; diff --git a/client/app/visualizations/counter/Editor/FormatSettings.jsx b/client/app/visualizations/counter/Editor/FormatSettings.jsx deleted file mode 100644 index a64ee27ad0..0000000000 --- a/client/app/visualizations/counter/Editor/FormatSettings.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; -import { Section, Input, InputNumber, Switch } from "@/components/visualizations/editor"; -import { EditorPropTypes } from "@/visualizations/prop-types"; - -import { isValueNumber } from "../utils"; - -export default function FormatSettings({ options, data, onOptionsChange }) { - const inputsEnabled = isValueNumber(data.rows, options); - return ( - -
- onOptionsChange({ stringDecimal })} - /> -
- -
- onOptionsChange({ stringDecChar: e.target.value })} - /> -
- -
- onOptionsChange({ stringThouSep: e.target.value })} - /> -
- -
- onOptionsChange({ stringPrefix: e.target.value })} - /> -
- -
- onOptionsChange({ stringSuffix: e.target.value })} - /> -
- -
- onOptionsChange({ formatTargetValue })}> - Format Target Value - -
-
- ); -} - -FormatSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.jsx index 74833f0f5c..60226892be 100644 --- a/client/app/visualizations/counter/Editor/GeneralSettings.jsx +++ b/client/app/visualizations/counter/Editor/GeneralSettings.jsx @@ -1,9 +1,8 @@ -import { map } from "lodash"; import React from "react"; -import { Section, Select, Input, InputNumber, Switch } from "@/components/visualizations/editor"; +import { Section, Input, ContextHelp } from "@/components/visualizations/editor"; import { EditorPropTypes } from "@/visualizations/prop-types"; -export default function GeneralSettings({ options, data, visualizationName, onOptionsChange }) { +export default function GeneralSettings({ options, visualizationName, onOptionsChange }) { return (
@@ -11,7 +10,7 @@ export default function GeneralSettings({ options, data, visualizationName, onOp layout="horizontal" label="Counter Label" className="w-100" - data-test="Counter.General.Label" + data-test="Counter.CounterLabel" defaultValue={options.counterLabel} placeholder={visualizationName} onChange={e => onOptionsChange({ counterLabel: e.target.value })} @@ -19,70 +18,41 @@ export default function GeneralSettings({ options, data, visualizationName, onOp
- -
- -
- + Number Format + + } className="w-100" - data-test="Counter.General.ValueRowNumber" - defaultValue={options.rowNumber} - disabled={options.countRow} - onChange={rowNumber => onOptionsChange({ rowNumber })} + data-test="Counter.NumberFormat" + defaultValue={options.numberFormat} + onChange={e => onOptionsChange({ numberFormat: e.target.value })} />
- + data-test="Counter.DecimalCharacter" + defaultValue={options.stringDecChar} + onChange={e => onOptionsChange({ stringDecChar: e.target.value })} + />
- onOptionsChange({ targetRowNumber })} + data-test="Counter.ThousandsSeparator" + defaultValue={options.stringThouSep} + onChange={e => onOptionsChange({ stringThouSep: e.target.value })} />
- -
- onOptionsChange({ countRow })}> - Count Rows - -
); } diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx new file mode 100644 index 0000000000..ec152a8888 --- /dev/null +++ b/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx @@ -0,0 +1,64 @@ +import { after } from "lodash"; +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "../getOptions"; +import GeneralSettings from "./GeneralSettings"; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(options, done) { + const data = { + columns: [ + { name: "a", type: "number" }, + { name: "b", type: "number" }, + ], + rows: [{ a: 123, b: 987 }], + }; + + options = getOptions(options, data); + return enzyme.mount( + { + expect(changedOptions).toMatchSnapshot(); + done(); + }} + /> + ); +} + +describe("Visualizations -> Counter -> Editor -> General Settings", () => { + test("Changes Counter Label", done => { + const el = mount({}, done); + + findByTestID(el, "Counter.CounterLabel") + .last() + .find("input") + .simulate("change", { target: { value: "Custom Counter Label" } }); + }); + + test("Changes Number Format", done => { + // we will perform 3 actions, so call `done` after all of them completed + const el = mount({}, after(3, done)); + + findByTestID(el, "Counter.NumberFormat") + .last() + .find("input") + .simulate("change", { target: { value: "0,0.0000" } }); + + findByTestID(el, "Counter.DecimalCharacter") + .last() + .find("input") + .simulate("change", { target: { value: "-" } }); + + findByTestID(el, "Counter.ThousandsSeparator") + .last() + .find("input") + .simulate("change", { target: { value: "/" } }); + }); +}); diff --git a/client/app/visualizations/counter/Editor/PrimaryValueSettings.jsx b/client/app/visualizations/counter/Editor/PrimaryValueSettings.jsx new file mode 100644 index 0000000000..be6eb40d7a --- /dev/null +++ b/client/app/visualizations/counter/Editor/PrimaryValueSettings.jsx @@ -0,0 +1,20 @@ +import { omit } from "lodash"; +import React from "react"; +import { EditorPropTypes } from "@/visualizations/prop-types"; +import CounterValueOptions from "./CounterValueOptions"; +import counterTypes from "../counterTypes"; + +export default function PrimaryValueSettings({ options, data, onOptionsChange }) { + const onChange = primaryValue => onOptionsChange({ primaryValue }); + + return ( + + ); +} + +PrimaryValueSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx b/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx new file mode 100644 index 0000000000..26a101867e --- /dev/null +++ b/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx @@ -0,0 +1,83 @@ +import { after } from "lodash"; +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "../getOptions"; +import PrimaryValueSettings from "./PrimaryValueSettings"; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(options, done) { + const data = { + columns: [ + { name: "a", type: "number" }, + { name: "b", type: "number" }, + ], + rows: [{ a: 123, b: 987 }], + }; + + options = getOptions(options, data); + return enzyme.mount( + { + expect(changedOptions).toMatchSnapshot(); + done(); + }} + /> + ); +} + +describe("Visualizations -> Counter -> Editor -> Primary Value Settings", () => { + test("Changes Counter Type", done => { + const el = mount({ schemaVersion: 2, primaryValue: { type: "unused" } }, done); + + findByTestID(el, "Counter.CounterType") + .last() + .simulate("click"); + findByTestID(el, "Counter.CounterType.rowValue") + .last() + .simulate("click"); + }); + + test("Changes Counter Type Options", done => { + // we will perform 2 actions, so call `done` after all of them completed + const el = mount({ schemaVersion: 2, primaryValue: { type: "rowValue" } }, after(2, done)); + + findByTestID(el, "Counter.ColumnName") + .last() + .simulate("click"); + findByTestID(el, "Counter.ColumnName.a") + .last() + .simulate("click"); + + findByTestID(el, "Counter.RowNumber") + .last() + .find("input") + .simulate("change", { target: { value: "3" } }); + }); + + test("Changes Format Options", done => { + // we will perform 3 actions, so call `done` after all of them completed + const el = mount({ schemaVersion: 2, primaryValue: { type: "rowValue" } }, after(3, done)); + + findByTestID(el, "Counter.DisplayFormat") + .last() + .find("input") + .simulate("change", { target: { value: "{{ @@value_formatted }} ({{ @@value }})" } }); + + findByTestID(el, "Counter.ShowTooltip") + .last() + .find("input") + .simulate("change", { target: { checked: false } }); + + findByTestID(el, "Counter.TooltipFormat") + .last() + .find("input") + .simulate("change", { target: { value: "{{ @@value_formatted }} / {{ @@value }}" } }); + }); +}); diff --git a/client/app/visualizations/counter/Editor/SecondaryValueSettings.jsx b/client/app/visualizations/counter/Editor/SecondaryValueSettings.jsx new file mode 100644 index 0000000000..809a0e68e6 --- /dev/null +++ b/client/app/visualizations/counter/Editor/SecondaryValueSettings.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Section, Checkbox } from "@/components/visualizations/editor"; +import { EditorPropTypes } from "@/visualizations/prop-types"; +import CounterValueOptions from "./CounterValueOptions"; +import counterTypes from "../counterTypes"; + +export default function SecondaryValueSettings({ options, data, onOptionsChange }) { + const onChange = secondaryValue => onOptionsChange({ secondaryValue }); + + const disabled = options.secondaryValue.type === "unused"; + + return ( + + +
+ onChange({ show: e.target.checked })}> + Show Secondary Value + +
+
+ ); +} + +SecondaryValueSettings.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx b/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx new file mode 100644 index 0000000000..1fa152e28f --- /dev/null +++ b/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx @@ -0,0 +1,92 @@ +import { after } from "lodash"; +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "../getOptions"; +import SecondaryValueSettings from "./SecondaryValueSettings"; + +function findByTestID(wrapper, testId) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(options, done) { + const data = { + columns: [ + { name: "a", type: "number" }, + { name: "b", type: "number" }, + ], + rows: [{ a: 123, b: 987 }], + }; + + options = getOptions(options, data); + return enzyme.mount( + { + expect(changedOptions).toMatchSnapshot(); + done(); + }} + /> + ); +} + +describe("Visualizations -> Counter -> Editor -> Primary Value Settings", () => { + test("Changes Counter Type", done => { + const el = mount({ schemaVersion: 2, primaryValue: { type: "unused" } }, done); + + findByTestID(el, "Counter.CounterType") + .last() + .simulate("click"); + findByTestID(el, "Counter.CounterType.rowValue") + .last() + .simulate("click"); + }); + + test("Changes Counter Type Options", done => { + // we will perform 2 actions, so call `done` after all of them completed + const el = mount({ schemaVersion: 2, secondaryValue: { type: "rowValue" } }, after(2, done)); + + findByTestID(el, "Counter.ColumnName") + .last() + .simulate("click"); + findByTestID(el, "Counter.ColumnName.a") + .last() + .simulate("click"); + + findByTestID(el, "Counter.RowNumber") + .last() + .find("input") + .simulate("change", { target: { value: "3" } }); + }); + + test("Changes Format Options", done => { + // we will perform 3 actions, so call `done` after all of them completed + const el = mount({ schemaVersion: 2, secondaryValue: { type: "rowValue" } }, after(3, done)); + + findByTestID(el, "Counter.DisplayFormat") + .last() + .find("input") + .simulate("change", { target: { value: "{{ @@value_formatted }} ({{ @@value }})" } }); + + findByTestID(el, "Counter.ShowTooltip") + .last() + .find("input") + .simulate("change", { target: { checked: false } }); + + findByTestID(el, "Counter.TooltipFormat") + .last() + .find("input") + .simulate("change", { target: { value: "{{ @@value_formatted }} / {{ @@value }}" } }); + }); + + test("Hides Secondary Value", done => { + const el = mount({ schemaVersion: 2, secondaryValue: { show: true, type: "rowValue" } }, done); + + findByTestID(el, "Counter.ShowSecondaryValue") + .last() + .find("input") + .simulate("change", { target: { checked: false } }); + }); +}); diff --git a/client/app/visualizations/counter/Editor/__snapshots__/GeneralSettings.test.jsx.snap b/client/app/visualizations/counter/Editor/__snapshots__/GeneralSettings.test.jsx.snap new file mode 100644 index 0000000000..78d91a9385 --- /dev/null +++ b/client/app/visualizations/counter/Editor/__snapshots__/GeneralSettings.test.jsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Counter -> Editor -> General Settings Changes Counter Label 1`] = ` +Object { + "counterLabel": "Custom Counter Label", +} +`; + +exports[`Visualizations -> Counter -> Editor -> General Settings Changes Number Format 1`] = ` +Object { + "numberFormat": "0,0.0000", +} +`; + +exports[`Visualizations -> Counter -> Editor -> General Settings Changes Number Format 2`] = ` +Object { + "stringDecChar": "-", +} +`; + +exports[`Visualizations -> Counter -> Editor -> General Settings Changes Number Format 3`] = ` +Object { + "stringThouSep": "/", +} +`; diff --git a/client/app/visualizations/counter/Editor/__snapshots__/PrimaryValueSettings.test.jsx.snap b/client/app/visualizations/counter/Editor/__snapshots__/PrimaryValueSettings.test.jsx.snap new file mode 100644 index 0000000000..d6fc6f5a24 --- /dev/null +++ b/client/app/visualizations/counter/Editor/__snapshots__/PrimaryValueSettings.test.jsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type 1`] = ` +Object { + "primaryValue": Object { + "type": "rowValue", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type Options 1`] = ` +Object { + "primaryValue": Object { + "column": "a", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type Options 2`] = ` +Object { + "primaryValue": Object { + "rowNumber": 3, + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 1`] = ` +Object { + "primaryValue": Object { + "displayFormat": "{{ @@value_formatted }} ({{ @@value }})", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 2`] = ` +Object { + "primaryValue": Object { + "showTooltip": false, + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 3`] = ` +Object { + "primaryValue": Object { + "tooltipFormat": "{{ @@value_formatted }} / {{ @@value }}", + }, +} +`; diff --git a/client/app/visualizations/counter/Editor/__snapshots__/SecondaryValueSettings.test.jsx.snap b/client/app/visualizations/counter/Editor/__snapshots__/SecondaryValueSettings.test.jsx.snap new file mode 100644 index 0000000000..d3ab438054 --- /dev/null +++ b/client/app/visualizations/counter/Editor/__snapshots__/SecondaryValueSettings.test.jsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type 1`] = ` +Object { + "secondaryValue": Object { + "type": "rowValue", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type Options 1`] = ` +Object { + "secondaryValue": Object { + "column": "a", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Counter Type Options 2`] = ` +Object { + "secondaryValue": Object { + "rowNumber": 3, + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 1`] = ` +Object { + "secondaryValue": Object { + "displayFormat": "{{ @@value_formatted }} ({{ @@value }})", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 3`] = ` +Object { + "secondaryValue": Object { + "tooltipFormat": "{{ @@value_formatted }} / {{ @@value }}", + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Hides Secondary Value 1`] = ` +Object { + "secondaryValue": Object { + "show": false, + }, +} +`; + +exports[`Visualizations -> Counter -> Editor -> Primary Value Settings Changes Format Options 2`] = ` +Object { + "secondaryValue": Object { + "showTooltip": false, + }, +} +`; diff --git a/client/app/visualizations/counter/Editor/index.js b/client/app/visualizations/counter/Editor/index.js index ce4bce4ea6..49891682ce 100644 --- a/client/app/visualizations/counter/Editor/index.js +++ b/client/app/visualizations/counter/Editor/index.js @@ -1,9 +1,11 @@ import createTabbedEditor from "@/components/visualizations/editor/createTabbedEditor"; import GeneralSettings from "./GeneralSettings"; -import FormatSettings from "./FormatSettings"; +import PrimaryValueSettings from "./PrimaryValueSettings"; +import SecondaryValueSettings from "./SecondaryValueSettings"; export default createTabbedEditor([ { key: "General", title: "General", component: GeneralSettings }, - { key: "Format", title: "Format", component: FormatSettings }, + { key: "PrimaryValue", title: "Primary Value", component: PrimaryValueSettings }, + { key: "SecondaryValue", title: "Secondary Value", component: SecondaryValueSettings }, ]); diff --git a/client/app/visualizations/counter/Renderer.jsx b/client/app/visualizations/counter/Renderer.jsx index ea7cadf14e..ce3fc2e1db 100644 --- a/client/app/visualizations/counter/Renderer.jsx +++ b/client/app/visualizations/counter/Renderer.jsx @@ -1,10 +1,11 @@ -import { isFinite } from "lodash"; -import React, { useState, useEffect } from "react"; +import { isFinite, isString } from "lodash"; +import React, { useState, useEffect, useMemo } from "react"; import cx from "classnames"; +import Tooltip from "antd/lib/tooltip"; import resizeObserver from "@/services/resizeObserver"; import { RendererPropTypes } from "@/visualizations/prop-types"; -import { getCounterData } from "./utils"; +import getCounterData from "./getCounterData"; import "./render.less"; @@ -23,6 +24,17 @@ function getCounterScale(container) { return Number(isFinite(scale) ? scale : 1).toFixed(2); // keep only two decimal places } +function renderTooltip(tooltip, children) { + if (isString(tooltip) && tooltip !== "") { + return ( + + {children} + + ); + } + return children; +} + export default function Renderer({ data, options, visualizationName }) { const [scale, setScale] = useState("1.00"); const [container, setContainer] = useState(null); @@ -44,15 +56,11 @@ export default function Renderer({ data, options, visualizationName }) { } }, [data, options, container]); - const { - showTrend, - trendPositive, - counterValue, - counterValueTooltip, - targetValue, - targetValueTooltip, - counterLabel, - } = getCounterData(data.rows, options, visualizationName); + const { counterLabel, showTrend, trendPositive, primaryValue, secondaryValue } = useMemo( + () => getCounterData(data.rows, options, visualizationName), + [data.rows, options, visualizationName] + ); + return (
-
- {counterValue} -
- {targetValue && ( -
- ({targetValue}) -
- )} -
{counterLabel}
+ {primaryValue.display !== null && + renderTooltip( + primaryValue.tooltip, +
+ {primaryValue.display} +
+ )} + {secondaryValue.display !== null && + renderTooltip( + secondaryValue.tooltip, +
+ {secondaryValue.display} +
+ )} + {counterLabel !== null &&
{counterLabel}
}
diff --git a/client/app/visualizations/counter/Renderer.test.jsx b/client/app/visualizations/counter/Renderer.test.jsx new file mode 100644 index 0000000000..64b97d964d --- /dev/null +++ b/client/app/visualizations/counter/Renderer.test.jsx @@ -0,0 +1,183 @@ +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "./getOptions"; +import Renderer from "./Renderer"; + +function mount(options) { + const data = { + columns: [ + { name: "city", type: "string" }, + { name: "population", type: "number" }, + ], + rows: [ + { city: "New York City", population: 18604000 }, + { city: "Shanghai", population: 24484000 }, + { city: "Tokyo", population: 38140000 }, + ], + }; + + options = getOptions(options, data); + return enzyme.mount(); +} + +describe("Visualizations -> Counter -> Renderer", () => { + test("Invalid column", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "missing", + rowNumber: 0, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "unused", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Numeric Primary Value", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "unused", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Non-numeric Primary Value", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "city", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "unused", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Numeric Secondary Value", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Non-numeric Secondary Value", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "rowValue", + column: "city", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Trend positive", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 3, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("Trend negative", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + secondaryValue: { + type: "rowValue", + column: "population", + rowNumber: 3, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: false, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); + + test("With tooltips", () => { + const el = mount({ + schemaVersion: 2, + primaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: true, + }, + secondaryValue: { + type: "rowValue", + column: "population", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }} / {{ @@value }}", + showTooltip: true, + }, + }); + expect(el.find(".counter-visualization-container")).toMatchSnapshot(); + }); +}); diff --git a/client/app/visualizations/counter/__snapshots__/Renderer.test.jsx.snap b/client/app/visualizations/counter/__snapshots__/Renderer.test.jsx.snap new file mode 100644 index 0000000000..1c237ab743 --- /dev/null +++ b/client/app/visualizations/counter/__snapshots__/Renderer.test.jsx.snap @@ -0,0 +1,1345 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Counter -> Renderer Invalid column 1`] = ` +
+
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Non-numeric Primary Value 1`] = ` +
+
+
+
+ New York City / New York City +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Non-numeric Secondary Value 1`] = ` +
+
+
+
+ 18,604,000 / 18604000 +
+
+ New York City / New York City +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Numeric Primary Value 1`] = ` +
+
+
+
+ 18,604,000 / 18604000 +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Numeric Secondary Value 1`] = ` +
+
+
+
+ 18,604,000 / 18604000 +
+
+ 18,604,000 / 18604000 +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Trend negative 1`] = ` +
+
+
+
+ 18,604,000 / 18604000 +
+
+ 38,140,000 / 38140000 +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer Trend positive 1`] = ` +
+
+
+
+ 38,140,000 / 38140000 +
+
+ 18,604,000 / 18604000 +
+
+ Test +
+
+
+
+`; + +exports[`Visualizations -> Counter -> Renderer With tooltips 1`] = ` +
+
+
+ + + +
+ 18,604,000 / 18604000 +
+
+
+
+ + + +
+ 18,604,000 / 18604000 +
+
+
+
+
+ Test +
+
+
+
+`; diff --git a/client/app/visualizations/counter/counterTypes.js b/client/app/visualizations/counter/counterTypes.js new file mode 100644 index 0000000000..9085061256 --- /dev/null +++ b/client/app/visualizations/counter/counterTypes.js @@ -0,0 +1,63 @@ +import { map, max, min, sumBy } from "lodash"; + +// 0 - special case, use first record +// 1..N - 1-based record number from beginning (wraps if greater than dataset size) +// -1..-N - 1-based record number from end (wraps if greater than dataset size) +function getRowNumber(rowNumber, rowsCount) { + rowNumber = parseInt(rowNumber, 10) || 0; + if (rowNumber === 0) { + return rowNumber; + } + const wrappedIndex = (Math.abs(rowNumber) - 1) % rowsCount; + return rowNumber > 0 ? wrappedIndex : rowsCount - wrappedIndex - 1; +} + +// `name`: string +// Human-readable name of counter type. +// +// `getValue`: (rows, valueOptions) => [value, row?] +// Takes all query result rows as a first argument and value options (primary or secondary) as second. +// Returns an array with two items: corresponding counter value (primary or secondary) and +// optionally a row from query result. If `getValue` may return row in addition to counter +// value - `canReturnRow` should be set to `true` (see `rowValue` for the reference). +// +// `options`: string[] +// List of additional options to show in visualization editor for the particular counter type. + +export default { + unused: { + name: "Unused", + getValue: () => [undefined, null], + options: [], + }, + rowValue: { + name: "Row Value", + getValue: (rows, { column, rowNumber }) => { + const row = rows[getRowNumber(rowNumber, rows.length)]; + const value = row ? row[column] : undefined; + return [value, row]; + }, + canReturnRow: true, + options: ["column", "rowNumber"], + }, + countRows: { + name: "Count Rows", + getValue: rows => [rows.length, null], + options: [], + }, + sumRows: { + name: "Sum Values", + getValue: (rows, { column }) => [sumBy(rows, column), null], + options: ["column"], + }, + minValue: { + name: "Min Value", + getValue: (rows, { column }) => [min(map(rows, row => row[column])), null], + options: ["column"], + }, + maxValue: { + name: "Max Value", + getValue: (rows, { column }) => [max(map(rows, row => row[column])), null], + options: ["column"], + }, +}; diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/countRows/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/countRows/default.json new file mode 100644 index 0000000000..e4a36602af --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/countRows/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "countRows", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 3, + "display": "3", + "tooltip": "3" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/default.json new file mode 100644 index 0000000000..1d73ad53fc --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "maxValue", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 38140000, + "display": "38140000", + "tooltip": "38140000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/invalid-column.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/invalid-column.json new file mode 100644 index 0000000000..03c36696e3 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/invalid-column.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "maxValue", + "column": "missing", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": null, + "primaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/default.json new file mode 100644 index 0000000000..57cdb6c2b1 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "minValue", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/invalid-column.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/invalid-column.json new file mode 100644 index 0000000000..0452480284 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/invalid-column.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "minValue", + "column": "missing", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": null, + "primaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/default.json new file mode 100644 index 0000000000..55a40164a8 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 24484000, + "display": "24484000", + "tooltip": "24484000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/invalid-column.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/invalid-column.json new file mode 100644 index 0000000000..774ca97ddd --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/invalid-column.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "missing", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": null, + "primaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/negative-row-number.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/negative-row-number.json new file mode 100644 index 0000000000..4e7b949134 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/negative-row-number.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": -3, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/positive-row-number.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/positive-row-number.json new file mode 100644 index 0000000000..8b056b70f1 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/positive-row-number.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 3, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 38140000, + "display": "38140000", + "tooltip": "38140000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-negative-row-number.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-negative-row-number.json new file mode 100644 index 0000000000..6b81e2f48f --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-negative-row-number.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": -6, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-positive-row-number.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-positive-row-number.json new file mode 100644 index 0000000000..790ddc692b --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-positive-row-number.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 6, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 38140000, + "display": "38140000", + "tooltip": "38140000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/zero-row-number.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/zero-row-number.json new file mode 100644 index 0000000000..72b92e3b54 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/zero-row-number.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/default.json new file mode 100644 index 0000000000..5ffe920fec --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "sumRows", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 81228000, + "display": "81228000", + "tooltip": "81228000" + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/invalid-column.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/invalid-column.json new file mode 100644 index 0000000000..1cfd907806 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/invalid-column.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "sumRows", + "column": "missing", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": null, + "primaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/counter-types/unused/default.json b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/unused/default.json new file mode 100644 index 0000000000..a92c37f749 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/counter-types/unused/default.json @@ -0,0 +1,49 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": null, + "primaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "secondaryValue": { + "value": null, + "display": null, + "tooltip": null + }, + "showTrend": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/display-and-tooltip-format.json b/client/app/visualizations/counter/fixtures/getCounterData/display-and-tooltip-format.json new file mode 100644 index 0000000000..d17ca9c964 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/display-and-tooltip-format.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0,0.00", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }} ({{ @@value }})", + "showTooltip": true, + "tooltipFormat": "{{ @@value_formatted }} ({{ @@value }})" + }, + "secondaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "{{ @@value_formatted }} ({{ @@value }})", + "showTooltip": true, + "tooltipFormat": "{{ @@value_formatted }} ({{ @@value }})" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18,604,000.00 (18604000)", + "tooltip": "18,604,000.00 (18604000)" + }, + "secondaryValue": { + "value": 24484000, + "display": "24,484,000.00 (24484000)", + "tooltip": "24,484,000.00 (24484000)" + }, + "showTrend": true, + "trendPositive": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/hide-tooltip.json b/client/app/visualizations/counter/fixtures/getCounterData/hide-tooltip.json new file mode 100644 index 0000000000..9c5991e8b0 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/hide-tooltip.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0,0.00", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": false, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": false, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18,604,000.00", + "tooltip": null + }, + "secondaryValue": { + "value": 24484000, + "display": "24,484,000.00", + "tooltip": null + }, + "showTrend": true, + "trendPositive": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/primary-secondary-different-types.json b/client/app/visualizations/counter/fixtures/getCounterData/primary-secondary-different-types.json new file mode 100644 index 0000000000..31dc616334 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/primary-secondary-different-types.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "minValue", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "maxValue", + "column": "population", + "rowNumber": 0, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": 38140000, + "display": "38140000", + "tooltip": "38140000" + }, + "showTrend": true, + "trendPositive": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/trend-negative.json b/client/app/visualizations/counter/fixtures/getCounterData/trend-negative.json new file mode 100644 index 0000000000..7e3804a62f --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/trend-negative.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": 24484000, + "display": "24484000", + "tooltip": "24484000" + }, + "showTrend": true, + "trendPositive": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/trend-positive.json b/client/app/visualizations/counter/fixtures/getCounterData/trend-positive.json new file mode 100644 index 0000000000..49bc51a800 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/trend-positive.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 24484000, + "display": "24484000", + "tooltip": "24484000" + }, + "secondaryValue": { + "value": 18604000, + "display": "18604000", + "tooltip": "18604000" + }, + "showTrend": true, + "trendPositive": true + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-counter-label.json b/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-counter-label.json new file mode 100644 index 0000000000..a9f63a50c6 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-counter-label.json @@ -0,0 +1,38 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "Test Counter", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0,0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": null, + "rowNumber": 1, + "displayFormat": "({{ @@value_formatted }})", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Test Counter" + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-number-format.json b/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-number-format.json new file mode 100644 index 0000000000..9f195f43ed --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/uses-custom-number-format.json @@ -0,0 +1,50 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": "'", + "numberFormat": "0,0.00", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 2, + "displayFormat": "({{ @@value }})", + "showTooltip": true, + "tooltipFormat": "{{ @@value_formatted }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name", + "primaryValue": { + "value": 18604000, + "display": "18'604'000.00", + "tooltip": "18604000" + }, + "secondaryValue": { + "value": 24484000, + "display": "(24484000)", + "tooltip": "24'484'000.00" + }, + "showTrend": true, + "trendPositive": false + } +} diff --git a/client/app/visualizations/counter/fixtures/getCounterData/uses-default-counter-label.json b/client/app/visualizations/counter/fixtures/getCounterData/uses-default-counter-label.json new file mode 100644 index 0000000000..3695578056 --- /dev/null +++ b/client/app/visualizations/counter/fixtures/getCounterData/uses-default-counter-label.json @@ -0,0 +1,38 @@ +{ + "input": { + "visualizationName": "Default Counter Name", + "data": [ + { "city": "New York City", "population": 18604000 }, + { "city": "Shanghai", "population": 24484000 }, + { "city": "Tokyo", "population": 38140000 } + ], + "options": { + "counterLabel": "", + "stringDecChar": ".", + "stringThouSep": ",", + "numberFormat": "0,0", + + "primaryValue": { + "show": true, + "type": "rowValue", + "column": "population", + "rowNumber": 1, + "displayFormat": "{{ @@value_formatted }}", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + }, + "secondaryValue": { + "show": true, + "type": "unused", + "column": null, + "rowNumber": 1, + "displayFormat": "({{ @@value_formatted }})", + "showTooltip": true, + "tooltipFormat": "{{ @@value }}" + } + } + }, + "output": { + "counterLabel": "Default Counter Name" + } +} diff --git a/client/app/visualizations/counter/getCounterData.js b/client/app/visualizations/counter/getCounterData.js new file mode 100644 index 0000000000..535f69df5d --- /dev/null +++ b/client/app/visualizations/counter/getCounterData.js @@ -0,0 +1,72 @@ +import { isNil, isNumber, isFinite, toString, invoke, extend } from "lodash"; +import numeral from "numeral"; +import { formatSimpleTemplate } from "@/lib/value-format"; +import counterTypes from "./counterTypes"; + +function formatValue(value, { numberFormat, stringDecChar, stringThouSep }) { + if (!isNumber(value)) { + return toString(value); + } + + // Temporarily update locale data (restore defaults after formatting) + const locale = numeral.localeData(); + const savedDelimiters = locale.delimiters; + + if (stringDecChar || stringThouSep) { + locale.delimiters = { + thousands: stringThouSep, + decimal: stringDecChar || ".", + }; + } + const result = numeral(value).format(numberFormat); + + locale.delimiters = savedDelimiters; + return result; +} + +function getCounterValue(rows, valueOptions, counterOptions) { + const [value, additionalFields] = invoke(counterTypes[valueOptions.type], "getValue", rows, valueOptions); + + if (!valueOptions.show || isNil(value)) { + return { value: null, display: null, tooltip: null }; + } + + const formatData = extend({}, additionalFields, { + "@@value": toString(value), + "@@value_formatted": isFinite(value) ? formatValue(value, counterOptions) : toString(value), + }); + + const display = formatSimpleTemplate(valueOptions.displayFormat, formatData); + const tooltip = valueOptions.showTooltip ? formatSimpleTemplate(valueOptions.tooltipFormat, formatData) : null; + + return { + value, + display: display !== "" ? display : null, + tooltip: tooltip !== "" ? tooltip : null, + }; +} + +export default function getCounterData(rows, options, visualizationName) { + const result = { + counterLabel: null, + primaryValue: getCounterValue(rows, options.primaryValue, options), + secondaryValue: getCounterValue(rows, options.secondaryValue, options), + showTrend: false, + }; + + if (!isNil(result.primaryValue.value) || !isNil(result.secondaryValue.value)) { + result.counterLabel = toString(options.counterLabel); + if (result.counterLabel === "") { + result.counterLabel = visualizationName; + } + + // TODO: Make this logic configurable + if (isFinite(result.primaryValue.value) && isFinite(result.secondaryValue.value)) { + const delta = result.primaryValue.value - result.secondaryValue.value; + result.showTrend = true; + result.trendPositive = delta >= 0; + } + } + + return result; +} diff --git a/client/app/visualizations/counter/getCounterData.test.js b/client/app/visualizations/counter/getCounterData.test.js new file mode 100644 index 0000000000..a3642dc7ed --- /dev/null +++ b/client/app/visualizations/counter/getCounterData.test.js @@ -0,0 +1,166 @@ +import getCounterData from "./getCounterData"; + +describe("Visualizations", () => { + describe("Counter", () => { + describe("getCounterData", () => { + test("Uses Default Counter Label", () => { + const { input, output } = require("./fixtures/getCounterData/uses-default-counter-label"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data.counterLabel).toEqual(output.counterLabel); + }); + + test("Uses Custom Counter Label", () => { + const { input, output } = require("./fixtures/getCounterData/uses-custom-counter-label"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data.counterLabel).toEqual(output.counterLabel); + }); + + test("Uses Custom Number Format", () => { + const { input, output } = require("./fixtures/getCounterData/uses-custom-number-format"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Trend Positive", () => { + const { input, output } = require("./fixtures/getCounterData/trend-positive"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Trend Negative", () => { + const { input, output } = require("./fixtures/getCounterData/trend-negative"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Display and Tooltip Format", () => { + const { input, output } = require("./fixtures/getCounterData/display-and-tooltip-format"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Hide Tooltip", () => { + const { input, output } = require("./fixtures/getCounterData/hide-tooltip"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Different Types of Primary and Secondary Values", () => { + const { input, output } = require("./fixtures/getCounterData/primary-secondary-different-types"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + describe("Counter Types", () => { + describe("Unused", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/unused/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + + describe("Row Value", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/rowValue/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Invalid Column", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/rowValue/invalid-column"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Zero Row Number", () => { + // special case - take first row + const { input, output } = require("./fixtures/getCounterData/counter-types/rowValue/zero-row-number"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Positive Row Number", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/rowValue/positive-row-number"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Positive Row Number (Wrapped)", () => { + const { + input, + output, + } = require("./fixtures/getCounterData/counter-types/rowValue/wrapped-positive-row-number"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Negative Row Number", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/rowValue/negative-row-number"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Negative Row Number (Wrapped)", () => { + const { + input, + output, + } = require("./fixtures/getCounterData/counter-types/rowValue/wrapped-negative-row-number"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + + describe("Count Rows", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/countRows/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + + describe("Sum Values", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/sumRows/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Invalid Column", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/sumRows/invalid-column"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + + describe("Min Value", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/minValue/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Invalid Column", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/minValue/invalid-column"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + + describe("Max Value", () => { + test("Default", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/maxValue/default"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + + test("Invalid Column", () => { + const { input, output } = require("./fixtures/getCounterData/counter-types/maxValue/invalid-column"); + const data = getCounterData(input.data, input.options, input.visualizationName); + expect(data).toEqual(output); + }); + }); + }); + }); + }); +}); diff --git a/client/app/visualizations/counter/getOptions/index.js b/client/app/visualizations/counter/getOptions/index.js new file mode 100644 index 0000000000..8256385db0 --- /dev/null +++ b/client/app/visualizations/counter/getOptions/index.js @@ -0,0 +1,3 @@ +import getOptions from "./v2"; + +export default getOptions; diff --git a/client/app/visualizations/counter/getOptions/v1.js b/client/app/visualizations/counter/getOptions/v1.js new file mode 100644 index 0000000000..7672fa1022 --- /dev/null +++ b/client/app/visualizations/counter/getOptions/v1.js @@ -0,0 +1,22 @@ +import { extend } from "lodash"; + +const schemaVersion = 1; + +const defaultOptions = { + counterLabel: "", + counterColName: "counter", + rowNumber: 1, + targetColName: null, + targetRowNumber: 1, + stringDecimal: 0, + stringDecChar: ".", + stringThouSep: ",", + stringPrefix: null, + stringSuffix: null, + formatTargetValue: false, + countRow: false, +}; + +export default function getOptions(options) { + return extend({}, defaultOptions, options, { schemaVersion }); +} diff --git a/client/app/visualizations/counter/getOptions/v2.js b/client/app/visualizations/counter/getOptions/v2.js new file mode 100644 index 0000000000..5b4b692c85 --- /dev/null +++ b/client/app/visualizations/counter/getOptions/v2.js @@ -0,0 +1,88 @@ +import { isEmpty, isFinite, merge, get, pick, padEnd, toString, includes, map } from "lodash"; +import getOptionsV1 from "./v1"; + +const schemaVersion = 2; + +const defaultOptions = { + schemaVersion, + + counterLabel: "", + + stringDecChar: ".", + stringThouSep: ",", + numberFormat: "0,0", + + primaryValue: { + show: true, + type: "rowValue", + column: "counter", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }}", + showTooltip: true, + tooltipFormat: "{{ @@value }}", + }, + secondaryValue: { + show: true, + type: "unused", + column: null, + rowNumber: 1, + displayFormat: "({{ @@value_formatted }})", + showTooltip: true, + tooltipFormat: "{{ @@value }}", + }, +}; + +function migrateFromV1(options) { + options = getOptionsV1(options); + const result = pick(options, ["counterLabel", "stringDecChar", "stringThouSep"]); + + result.numberFormat = "0,0.000"; + if (isFinite(options.stringDecimal) && options.stringDecimal >= 0) { + result.numberFormat = "0,0"; + if (options.stringDecimal > 0) { + const decimals = padEnd("", options.stringDecimal, "0"); + result.numberFormat = `${result.numberFormat}.${decimals}`; + } + } + + const prefix = toString(options.stringPrefix); + const suffix = toString(options.stringSuffix); + + result.primaryValue = { + show: true, + type: options.countRow ? "countRows" : "rowValue", + column: options.counterColName, + rowNumber: options.rowNumber, + displayFormat: `${prefix}{{ @@value_formatted }}${suffix}`, + }; + + result.secondaryValue = { + show: true, + type: options.targetColName ? "rowValue" : "unused", + column: options.targetColName, + rowNumber: options.targetRowNumber, + displayFormat: options.formatTargetValue ? `(${prefix}{{ @@value_formatted }}${suffix})` : "({{ @@value }})", + }; + + return result; +} + +export default function getOptions(options, { columns }) { + const currentSchemaVersion = get(options, "schemaVersion", isEmpty(options) ? schemaVersion : 0); + if (currentSchemaVersion < schemaVersion) { + options = migrateFromV1(options); + } + + const optionsWithDefaults = merge({}, defaultOptions, options, { schemaVersion }); + const columnNameUpdates = {}; + + const columnNames = map(columns, col => col.name); + if (!includes(columnNames, get(optionsWithDefaults, "primaryValue.column"))) { + columnNameUpdates.primaryValue = { column: null }; + } + if (!includes(columnNames, get(optionsWithDefaults, "secondaryValue.column"))) { + columnNameUpdates.secondaryValue = { column: null }; + } + + return merge({}, optionsWithDefaults, columnNameUpdates); +} diff --git a/client/app/visualizations/counter/index.js b/client/app/visualizations/counter/index.js index 22b9932267..18e95a07c2 100644 --- a/client/app/visualizations/counter/index.js +++ b/client/app/visualizations/counter/index.js @@ -1,21 +1,11 @@ import Renderer from "./Renderer"; import Editor from "./Editor"; - -const DEFAULT_OPTIONS = { - counterLabel: "", - counterColName: "counter", - rowNumber: 1, - targetRowNumber: 1, - stringDecimal: 0, - stringDecChar: ".", - stringThouSep: ",", - tooltipFormat: "0,0.000", // TODO: Show in editor -}; +import getOptions from "./getOptions"; export default { type: "COUNTER", name: "Counter", - getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }), + getOptions, Renderer, Editor, diff --git a/client/app/visualizations/counter/render.less b/client/app/visualizations/counter/render.less index 252d0c0242..fd0bf0500c 100755 --- a/client/app/visualizations/counter/render.less +++ b/client/app/visualizations/counter/render.less @@ -43,4 +43,12 @@ &.trend-negative .counter-visualization-value { color: #d9534f; } + + .counter-visualization-value.with-tooltip, + .counter-visualization-target.with-tooltip { + &:hover { + background: rgba(0, 0, 0, 0.02); + border-radius: 3px; + } + } } diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js deleted file mode 100644 index 666bbfe476..0000000000 --- a/client/app/visualizations/counter/utils.js +++ /dev/null @@ -1,133 +0,0 @@ -import { isNumber, isFinite, toString } from "lodash"; -import numeral from "numeral"; - -// TODO: allow user to specify number format string instead of delimiters only -// It will allow to remove this function (move all that weird formatting logic to a migration -// that will set number format for all existing counter visualization) -function numberFormat(value, decimalPoints, decimalDelimiter, thousandsDelimiter) { - // Temporarily update locale data (restore defaults after formatting) - const locale = numeral.localeData(); - const savedDelimiters = locale.delimiters; - - // Mimic old behavior - AngularJS `number` filter defaults: - // - `,` as thousands delimiter - // - `.` as decimal delimiter - // - three decimal points - locale.delimiters = { - thousands: ",", - decimal: ".", - }; - let formatString = "0,0.000"; - if ((Number.isFinite(decimalPoints) && decimalPoints >= 0) || decimalDelimiter || thousandsDelimiter) { - locale.delimiters = { - thousands: thousandsDelimiter, - decimal: decimalDelimiter || ".", - }; - - formatString = "0,0"; - if (decimalPoints > 0) { - formatString += "."; - while (decimalPoints > 0) { - formatString += "0"; - decimalPoints -= 1; - } - } - } - const result = numeral(value).format(formatString); - - locale.delimiters = savedDelimiters; - return result; -} - -// 0 - special case, use first record -// 1..N - 1-based record number from beginning (wraps if greater than dataset size) -// -1..-N - 1-based record number from end (wraps if greater than dataset size) -function getRowNumber(index, rowsCount) { - index = parseInt(index, 10) || 0; - if (index === 0) { - return index; - } - const wrappedIndex = (Math.abs(index) - 1) % rowsCount; - return index > 0 ? wrappedIndex : rowsCount - wrappedIndex - 1; -} - -function formatValue(value, { stringPrefix, stringSuffix, stringDecimal, stringDecChar, stringThouSep }) { - if (isNumber(value)) { - value = numberFormat(value, stringDecimal, stringDecChar, stringThouSep); - return toString(stringPrefix) + value + toString(stringSuffix); - } - return toString(value); -} - -function formatTooltip(value, formatString) { - if (isNumber(value)) { - return numeral(value).format(formatString); - } - return toString(value); -} - -export function getCounterData(rows, options, visualizationName) { - const result = {}; - const rowsCount = rows.length; - - if (rowsCount > 0 || options.countRow) { - const counterColName = options.counterColName; - const targetColName = options.targetColName; - - result.counterLabel = options.counterLabel || visualizationName; - - if (options.countRow) { - result.counterValue = rowsCount; - } else if (counterColName) { - const rowNumber = getRowNumber(options.rowNumber, rowsCount); - result.counterValue = rows[rowNumber][counterColName]; - } - - result.showTrend = false; - - if (targetColName) { - const targetRowNumber = getRowNumber(options.targetRowNumber, rowsCount); - result.targetValue = rows[targetRowNumber][targetColName]; - - if (Number.isFinite(result.counterValue) && isFinite(result.targetValue)) { - const delta = result.counterValue - result.targetValue; - result.showTrend = true; - result.trendPositive = delta >= 0; - } - } else { - result.targetValue = null; - } - - result.counterValueTooltip = formatTooltip(result.counterValue, options.tooltipFormat); - result.targetValueTooltip = formatTooltip(result.targetValue, options.tooltipFormat); - - result.counterValue = formatValue(result.counterValue, options); - - if (options.formatTargetValue) { - result.targetValue = formatValue(result.targetValue, options); - } else { - if (isFinite(result.targetValue)) { - result.targetValue = numeral(result.targetValue).format("0[.]00[0]"); - } - } - } - - return result; -} - -export function isValueNumber(rows, options) { - if (options.countRow) { - return true; // array length is always a number - } - - const rowsCount = rows.length; - if (rowsCount > 0) { - const rowNumber = getRowNumber(options.rowNumber, rowsCount); - const counterColName = options.counterColName; - if (counterColName) { - return isNumber(rows[rowNumber][counterColName]); - } - } - - return false; -} diff --git a/client/app/visualizations/counter/utils.test.js b/client/app/visualizations/counter/utils.test.js deleted file mode 100644 index 561d4162eb..0000000000 --- a/client/app/visualizations/counter/utils.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import { getCounterData } from "./utils"; - -let dummy; - -describe("Visualizations -> Counter -> Utils", () => { - beforeEach(() => { - dummy = { - rows: [ - { city: "New York City", population: 18604000 }, - { city: "Shangai", population: 24484000 }, - { city: "Tokyo", population: 38140000 }, - ], - options: {}, - visualisationName: "Visualisation Name", - result: { - counterLabel: "Visualisation Name", - counterValue: "", - targetValue: null, - counterValueTooltip: "", - targetValueTooltip: "", - }, - }; - }); - - describe("getCounterData()", () => { - describe('"Count rows" option is disabled', () => { - test("No target and counter values return empty result", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualisationName); - expect(result).toEqual({ - ...dummy.result, - showTrend: false, - }); - }); - - test('"Counter label" overrides vizualization name', () => { - const result = getCounterData(dummy.rows, { counterLabel: "Counter Label" }, dummy.visualisationName); - expect(result).toEqual({ - ...dummy.result, - counterLabel: "Counter Label", - showTrend: false, - }); - }); - - test('"Counter Value Column Name" must be set to a correct non empty value', () => { - const result = getCounterData(dummy.rows, { rowNumber: 3 }, dummy.visualisationName); - expect(result).toEqual({ - ...dummy.result, - showTrend: false, - }); - - const result2 = getCounterData(dummy.rows, { counterColName: "missingColumn" }, dummy.visualisationName); - expect(result2).toEqual({ - ...dummy.result, - showTrend: false, - }); - }); - - test('"Counter Value Column Name" uses correct column', () => { - const result = getCounterData(dummy.rows, { counterColName: "population" }, dummy.visualisationName); - expect(result).toEqual({ - ...dummy.result, - counterValue: "18,604,000.000", - counterValueTooltip: "18,604,000", - showTrend: false, - }); - }); - - test("Counter and target values return correct result including trend", () => { - const result = getCounterData( - dummy.rows, - { - rowNumber: 1, - counterColName: "population", - targetRowNumber: 2, - targetColName: "population", - }, - dummy.visualisationName - ); - expect(result).toEqual({ - ...dummy.result, - counterValue: "18,604,000.000", - counterValueTooltip: "18,604,000", - targetValue: "24484000", - targetValueTooltip: "24,484,000", - showTrend: true, - trendPositive: false, - }); - - const result2 = getCounterData( - dummy.rows, - { - rowNumber: 2, - counterColName: "population", - targetRowNumber: 1, - targetColName: "population", - }, - dummy.visualisationName - ); - expect(result2).toEqual({ - ...dummy.result, - counterValue: "24,484,000.000", - counterValueTooltip: "24,484,000", - targetValue: "18604000", - targetValueTooltip: "18,604,000", - showTrend: true, - trendPositive: true, - }); - }); - }); - - describe('"Count rows" option is enabled', () => { - beforeEach(() => { - dummy.result = { - ...dummy.result, - counterValue: "3.000", - counterValueTooltip: "3", - showTrend: false, - }; - }); - - test("Rows are counted correctly", () => { - const result = getCounterData(dummy.rows, { countRow: true }, dummy.visualisationName); - expect(result).toEqual(dummy.result); - }); - - test("Counter value is ignored", () => { - const result = getCounterData( - dummy.rows, - { - countRow: true, - rowNumber: 3, - counterColName: "population", - }, - dummy.visualisationName - ); - expect(result).toEqual(dummy.result); - }); - - test("Target value and trend are computed correctly", () => { - const result = getCounterData( - dummy.rows, - { - countRow: true, - targetRowNumber: 2, - targetColName: "population", - }, - dummy.visualisationName - ); - expect(result).toEqual({ - ...dummy.result, - targetValue: "24484000", - targetValueTooltip: "24,484,000", - showTrend: true, - trendPositive: false, - }); - }); - - test("Empty rows return counter value 0", () => { - const result = getCounterData([], { countRow: true }, dummy.visualisationName); - expect(result).toEqual({ - ...dummy.result, - counterValue: "0.000", - counterValueTooltip: "0", - }); - }); - }); - }); -}); diff --git a/client/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js index b52f8454f4..257c9c2598 100644 --- a/client/cypress/integration/visualizations/counter_spec.js +++ b/client/cypress/integration/visualizations/counter_spec.js @@ -1,223 +1,149 @@ /* global cy, Cypress */ -import { createQuery } from "../../support/redash-api"; +import { createQuery, createVisualization } from "../../support/redash-api"; + +const { merge } = Cypress._; const SQL = ` SELECT 27182.8182846 AS a, 20000 AS b, 'lorem' AS c UNION ALL SELECT 31415.9265359 AS a, 40000 AS b, 'ipsum' AS c `; +const counterOptions = { + schemaVersion: 2, + + primaryValue: { + type: "rowValue", + column: "counter", + rowNumber: 1, + displayFormat: "{{ @@value_formatted }}", + showTooltip: true, + tooltipFormat: "{{ @@value }}", + }, + secondaryValue: { + show: true, + type: "unused", + column: null, + rowNumber: 1, + displayFormat: "({{ @@value_formatted }})", + showTooltip: true, + tooltipFormat: "{{ @@value }}", + }, +}; + describe("Counter", () => { const viewportWidth = Cypress.config("viewportWidth"); beforeEach(() => { cy.login(); - createQuery({ query: SQL }).then(({ id }) => { - cy.visit(`queries/${id}/source`); - cy.getByTestId("ExecuteButton").click(); - }); - }); - - it("creates simple Counter", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - `); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (with defaults)", { widths: [viewportWidth] }); - }); - - it("creates Counter with custom label", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - `); - - cy.fillInputs({ - "Counter.General.Label": "Custom Label", - }); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (custom label)", { widths: [viewportWidth] }); }); - it("creates Counter with non-numeric value", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.c - - Counter.General.TargetValueColumn - Counter.General.TargetValueColumn.c - `); - - cy.fillInputs({ - "Counter.General.TargetValueRowNumber": "2", - }); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (non-numeric value)", { widths: [viewportWidth] }); + it("creates Counter (custom formatting)", () => { + createQuery({ query: SQL }) + .then(({ id }) => + createVisualization( + id, + "COUNTER", + "Counter", + merge({}, counterOptions, { + counterLabel: "Test", + stringDecChar: ",", + stringThouSep: "'", + numberFormat: "0,0.000", + + primaryValue: { + type: "rowValue", + column: "a", + rowNumber: 1, + displayFormat: "$$ {{ @@value_formatted }} %%", + }, + secondaryValue: { + type: "unused", + }, + }) + ) + ) + .then(({ id: visualizationId, query_id: queryId }) => { + cy.visit(`queries/${queryId}/source#${visualizationId}`); + cy.getByTestId("ExecuteButton").click(); + + cy.getByTestId(`QueryPageVisualization${visualizationId}`) + .find(".counter-visualization-container") + .should("exist"); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot("Visualizations - Counter (custom formatting)", { widths: [viewportWidth] }); + }); }); - it("creates Counter with target value (trend positive)", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.TargetValueColumn - Counter.General.TargetValueColumn.b - `); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (target value + trend positive)", { widths: [viewportWidth] }); - }); - - it("creates Counter with custom row number (trend negative)", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.TargetValueColumn - Counter.General.TargetValueColumn.b - `); - - cy.fillInputs({ - "Counter.General.ValueRowNumber": "2", - "Counter.General.TargetValueRowNumber": "2", - }); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (row number + trend negative)", { widths: [viewportWidth] }); - }); - - it("creates Counter with count rows", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.CountRows - `); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (count rows)", { widths: [viewportWidth] }); - }); - - it("creates Counter with formatting", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.TargetValueColumn - Counter.General.TargetValueColumn.b - - VisualizationEditor.Tabs.Format - `); - - cy.fillInputs({ - "Counter.Formatting.DecimalPlace": "4", - "Counter.Formatting.DecimalCharacter": ",", - "Counter.Formatting.ThousandsSeparator": "`", - "Counter.Formatting.StringPrefix": "$", - "Counter.Formatting.StringSuffix": "%", - }); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (custom formatting)", { widths: [viewportWidth] }); + it("creates Counter (trend positive)", () => { + createQuery({ query: SQL }) + .then(({ id }) => + createVisualization( + id, + "COUNTER", + "Counter", + merge({}, counterOptions, { + primaryValue: { + type: "rowValue", + column: "b", + rowNumber: 2, + }, + secondaryValue: { + type: "rowValue", + column: "b", + rowNumber: 1, + }, + }) + ) + ) + .then(({ id: visualizationId, query_id: queryId }) => { + cy.visit(`queries/${queryId}/source#${visualizationId}`); + cy.getByTestId("ExecuteButton").click(); + + cy.getByTestId(`QueryPageVisualization${visualizationId}`) + .find(".counter-visualization-container") + .should("exist"); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot("Visualizations - Counter (trend positive)", { widths: [viewportWidth] }); + }); }); - it("creates Counter with target value formatting", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.TargetValueColumn - Counter.General.TargetValueColumn.b - - VisualizationEditor.Tabs.Format - Counter.Formatting.FormatTargetValue - `); - - cy.fillInputs({ - "Counter.Formatting.DecimalPlace": "4", - "Counter.Formatting.DecimalCharacter": ",", - "Counter.Formatting.ThousandsSeparator": "`", - "Counter.Formatting.StringPrefix": "$", - "Counter.Formatting.StringSuffix": "%", - }); - - cy.getByTestId("VisualizationPreview") - .find(".counter-visualization-container") - .should("exist"); - - // wait a bit before taking snapshot - cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting - cy.percySnapshot("Visualizations - Counter (format target value)", { widths: [viewportWidth] }); + it("creates Counter (trend negative)", () => { + createQuery({ query: SQL }) + .then(({ id }) => + createVisualization( + id, + "COUNTER", + "Counter", + merge({}, counterOptions, { + primaryValue: { + type: "rowValue", + column: "b", + rowNumber: 1, + }, + secondaryValue: { + type: "rowValue", + column: "b", + rowNumber: 2, + }, + }) + ) + ) + .then(({ id: visualizationId, query_id: queryId }) => { + cy.visit(`queries/${queryId}/source#${visualizationId}`); + cy.getByTestId("ExecuteButton").click(); + + cy.getByTestId(`QueryPageVisualization${visualizationId}`) + .find(".counter-visualization-container") + .should("exist"); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot("Visualizations - Counter (trend negative)", { widths: [viewportWidth] }); + }); }); }); diff --git a/migrations/versions/c0cbaae98215_add_type_to_counter_visualization.py b/migrations/versions/c0cbaae98215_add_type_to_counter_visualization.py new file mode 100644 index 0000000000..330f60589d --- /dev/null +++ b/migrations/versions/c0cbaae98215_add_type_to_counter_visualization.py @@ -0,0 +1,71 @@ +"""Add type to counter visualization + +Revision ID: c0cbaae98215 +Revises: e5c7a4e2df4d +Create Date: 2019-11-25 14:08:05.155120 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table + + +from redash.models import MutableDict, PseudoJSON + + +# revision identifiers, used by Alembic. +revision = 'c0cbaae98215' +down_revision = 'e5c7a4e2df4d' +branch_labels = None +depends_on = None + + +def upgrade(): + visualizations = table( + 'visualizations', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('type', sa.Unicode(length=100), nullable=False), + sa.Column('options', MutableDict.as_mutable(PseudoJSON))) + + conn = op.get_bind() + for visualization in conn.execute(visualizations.select().where(visualizations.c.type == 'COUNTER')): + options = visualization.options + # map existing counters to countRows type when countRow is true + if 'countRow' in options and options['countRow']: + options['counterType'] = 'countRows' + else: + options['counterType'] = 'rowValue' + + # remove countRow from options + options.pop('countRow', None) + + conn.execute( + visualizations + .update() + .where(visualizations.c.id == visualization.id) + .values(options=MutableDict(options))) + +def downgrade(): + visualizations = table( + 'visualizations', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('type', sa.Unicode(length=100), nullable=False), + sa.Column('options', MutableDict.as_mutable(PseudoJSON))) + + conn = op.get_bind() + for visualization in conn.execute(visualizations.select().where(visualizations.c.type == 'COUNTER')): + options = visualization.options + # use countRow option when counterType is 'countRows' + if 'counterType' in options and options['counterType'] == 'countRows': + options['countRow'] = True + else: + options['countRow'] = False + + # remove counterType from options + options.pop('counterType', None) + + conn.execute( + visualizations + .update() + .where(visualizations.c.id == visualization.id) + .values(options=MutableDict(options)))