diff --git a/client/app/assets/less/inc/visualizations/counter-render.less b/client/app/assets/less/inc/visualizations/counter-render.less
deleted file mode 100755
index c57c297a98..0000000000
--- a/client/app/assets/less/inc/visualizations/counter-render.less
+++ /dev/null
@@ -1,45 +0,0 @@
-counter-renderer {
- display: block;
- text-align: center;
- padding: 15px 10px;
- overflow: hidden;
-
- counter {
- margin: 0;
- padding: 0;
- font-size: 80px;
- line-height: normal;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
-
- value,
- counter-target {
- font-size: 1em;
- display: block;
- }
-
- counter-name {
- font-size: 0.5em;
- display: block;
- }
-
- &.positive value {
- color: #5cb85c;
- }
-
- &.negative value {
- color: #d9534f;
- }
- }
-
- counter-target {
- color: #ccc;
- }
-
- counter-name {
- font-size: 0.5em;
- display: block;
- }
-}
diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less
index f0c92409fa..8b0b26f3e6 100644
--- a/client/app/assets/less/main.less
+++ b/client/app/assets/less/main.less
@@ -53,7 +53,6 @@
@import 'inc/schema-browser';
@import 'inc/toast';
@import 'inc/visualizations/box';
-@import 'inc/visualizations/counter-render';
@import 'inc/visualizations/sankey';
@import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map';
diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less
index a644de4ba8..e672fada81 100644
--- a/client/app/pages/dashboards/dashboard.less
+++ b/client/app/pages/dashboards/dashboard.less
@@ -91,7 +91,7 @@
overflow: hidden;
}
- counter {
+ .counter-visualization-content {
position: absolute;
left: 10px;
top: 15px;
@@ -145,7 +145,7 @@
.dashboard__control {
margin: 8px 0;
-
+
.save-status {
vertical-align: middle;
margin-right: 7px;
diff --git a/client/app/visualizations/counter/Editor.jsx b/client/app/visualizations/counter/Editor.jsx
new file mode 100644
index 0000000000..24827bb199
--- /dev/null
+++ b/client/app/visualizations/counter/Editor.jsx
@@ -0,0 +1,236 @@
+import { merge, map } from 'lodash';
+import React from 'react';
+import Tabs from 'antd/lib/tabs';
+import Input from 'antd/lib/input';
+import InputNumber from 'antd/lib/input-number';
+import Select from 'antd/lib/select';
+import Switch from 'antd/lib/switch';
+import * as Grid from 'antd/lib/grid';
+import { EditorPropTypes } from '@/visualizations';
+
+import { isValueNumber } from './utils';
+
+function GeneralSettings({ options, data, visualizationName, onOptionsChange }) {
+ return (
+
+
+
+
+
+
+ onOptionsChange({ counterLabel: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onOptionsChange({ rowNumber })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onOptionsChange({ targetRowNumber })}
+ />
+
+
+
+
+
+ );
+}
+
+GeneralSettings.propTypes = EditorPropTypes;
+
+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 })}
+ />
+
+
+
+
+
+ );
+}
+
+FormatSettings.propTypes = EditorPropTypes;
+
+export default function Editor(props) {
+ const { options, onOptionsChange } = props;
+
+ const optionsChanged = (newOptions) => {
+ onOptionsChange(merge({}, options, newOptions));
+ };
+
+ return (
+
+ General}>
+
+
+ Format}>
+
+
+
+ );
+}
+
+Editor.propTypes = EditorPropTypes;
diff --git a/client/app/visualizations/counter/Renderer.jsx b/client/app/visualizations/counter/Renderer.jsx
new file mode 100644
index 0000000000..417ef24edb
--- /dev/null
+++ b/client/app/visualizations/counter/Renderer.jsx
@@ -0,0 +1,73 @@
+import { isFinite } from 'lodash';
+import React, { useState, useEffect } from 'react';
+import cx from 'classnames';
+import resizeObserver from '@/services/resizeObserver';
+import { RendererPropTypes } from '@/visualizations';
+
+import { getCounterData } from './utils';
+
+import './render.less';
+
+function getCounterStyles(scale) {
+ return {
+ msTransform: `scale(${scale})`,
+ MozTransform: `scale(${scale})`,
+ WebkitTransform: `scale(${scale})`,
+ transform: `scale(${scale})`,
+ };
+}
+
+function getCounterScale(container) {
+ const inner = container.firstChild;
+ const scale = Math.min(container.offsetWidth / inner.offsetWidth, container.offsetHeight / inner.offsetHeight);
+ return Number(isFinite(scale) ? scale : 1).toFixed(2); // keep only two decimal places
+}
+
+export default function Renderer({ data, options, visualizationName }) {
+ const [scale, setScale] = useState('1.00');
+ const [container, setContainer] = useState(null);
+
+ useEffect(() => {
+ if (container) {
+ const unwatch = resizeObserver(container, () => {
+ setScale(getCounterScale(container));
+ });
+ return unwatch;
+ }
+ }, [container]);
+
+ useEffect(() => {
+ if (container) {
+ // update scaling when options or data change (new formatting, values, etc.
+ // may change inner container dimensions which will not be tracked by `resizeObserver`);
+ setScale(getCounterScale(container));
+ }
+ }, [data, options, container]);
+
+ const {
+ showTrend, trendPositive,
+ counterValue, counterValueTooltip,
+ targetValue, targetValueTooltip,
+ counterLabel,
+ } = getCounterData(data.rows, options, visualizationName);
+ return (
+
+
+
+
{counterValue}
+ {targetValue && (
+
({targetValue})
+ )}
+
{counterLabel}
+
+
+
+ );
+}
+
+Renderer.propTypes = RendererPropTypes;
diff --git a/client/app/visualizations/counter/counter-editor.html b/client/app/visualizations/counter/counter-editor.html
deleted file mode 100644
index f47238c4ac..0000000000
--- a/client/app/visualizations/counter/counter-editor.html
+++ /dev/null
@@ -1,95 +0,0 @@
-
diff --git a/client/app/visualizations/counter/counter.html b/client/app/visualizations/counter/counter.html
deleted file mode 100644
index d7493f4378..0000000000
--- a/client/app/visualizations/counter/counter.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- {{ counterValue }}
- ({{ targetValue }})
- {{counterLabel}}
-
-
diff --git a/client/app/visualizations/counter/index.js b/client/app/visualizations/counter/index.js
index 2ece76b2b2..07bd18af5d 100644
--- a/client/app/visualizations/counter/index.js
+++ b/client/app/visualizations/counter/index.js
@@ -1,10 +1,7 @@
-import { isNumber, toString } from 'lodash';
-import numeral from 'numeral';
-import { angular2react } from 'angular2react';
import { registerVisualization } from '@/visualizations';
-import counterTemplate from './counter.html';
-import counterEditorTemplate from './counter-editor.html';
+import Renderer from './Renderer';
+import Editor from './Editor';
const DEFAULT_OPTIONS = {
counterLabel: '',
@@ -17,207 +14,16 @@ const DEFAULT_OPTIONS = {
tooltipFormat: '0,0.000', // TODO: Show in editor
};
-// 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;
+export default function init() {
+ registerVisualization({
+ type: 'COUNTER',
+ name: 'Counter',
+ getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }),
+ Renderer,
+ Editor,
- // 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;
-}
-
-// TODO: Need to review this function, it does not properly handle edge cases.
-function getRowNumber(index, size) {
- if (index >= 0) {
- return index - 1;
- }
-
- if (Math.abs(index) > size) {
- index %= size;
- }
-
- return size + index;
-}
-
-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);
-}
-
-const CounterRenderer = {
- template: counterTemplate,
- bindings: {
- data: '<',
- options: '<',
- visualizationName: '<',
- },
- controller($scope, $element, $timeout) {
- $scope.fontSize = '1em';
-
- $scope.scale = 1;
- const root = $element[0].querySelector('counter');
- const container = $element[0].querySelector('counter > div');
- $scope.handleResize = () => {
- const scale = Math.min(root.offsetWidth / container.offsetWidth, root.offsetHeight / container.offsetHeight);
- $scope.scale = Math.floor(scale * 100) / 100; // keep only two decimal places
- };
-
- const update = () => {
- const options = this.options;
- const data = this.data.rows;
-
- if (data.length > 0) {
- const rowNumber = getRowNumber(options.rowNumber, data.length);
- const targetRowNumber = getRowNumber(options.targetRowNumber, data.length);
- const counterColName = options.counterColName;
- const targetColName = options.targetColName;
- const counterLabel = options.counterLabel;
-
- if (counterLabel) {
- $scope.counterLabel = counterLabel;
- } else {
- $scope.counterLabel = this.visualizationName;
- }
-
- if (options.countRow) {
- $scope.counterValue = data.length;
- } else if (counterColName) {
- $scope.counterValue = data[rowNumber][counterColName];
- }
-
- $scope.showTrend = false;
- if (targetColName) {
- $scope.targetValue = data[targetRowNumber][targetColName];
-
- if (Number.isFinite($scope.counterValue) && Number.isFinite($scope.targetValue)) {
- const delta = $scope.counterValue - $scope.targetValue;
- $scope.showTrend = true;
- $scope.trendPositive = delta >= 0;
- }
- } else {
- $scope.targetValue = null;
- }
-
- $scope.counterValueTooltip = formatTooltip($scope.counterValue, options.tooltipFormat);
- $scope.targetValueTooltip = formatTooltip($scope.targetValue, options.tooltipFormat);
-
- $scope.counterValue = formatValue($scope.counterValue, options);
-
- if (options.formatTargetValue) {
- $scope.targetValue = formatValue($scope.targetValue, options);
- } else {
- if (Number.isFinite($scope.targetValue)) {
- $scope.targetValue = numeral($scope.targetValue).format('0[.]00[0]');
- }
- }
- }
-
- $timeout(() => {
- $scope.handleResize();
- });
- };
-
- $scope.$watch('$ctrl.data', update);
- $scope.$watch('$ctrl.options', update, true);
- },
-};
-
-const CounterEditor = {
- template: counterEditorTemplate,
- bindings: {
- data: '<',
- options: '<',
- visualizationName: '<',
- onOptionsChange: '<',
- },
- controller($scope) {
- this.currentTab = 'general';
- this.changeTab = (tab) => {
- this.currentTab = tab;
- };
-
- this.isValueNumber = () => {
- const options = this.options;
- const data = this.data.rows;
-
- if (data.length > 0) {
- const rowNumber = getRowNumber(options.rowNumber, data.length);
- const counterColName = options.counterColName;
-
- if (options.countRow) {
- this.counterValue = data.length;
- } else if (counterColName) {
- this.counterValue = data[rowNumber][counterColName];
- }
- }
-
- return isNumber(this.counterValue);
- };
-
- $scope.$watch('$ctrl.options', (options) => {
- this.onOptionsChange(options);
- }, true);
- },
-};
-
-export default function init(ngModule) {
- ngModule.component('counterRenderer', CounterRenderer);
- ngModule.component('counterEditor', CounterEditor);
-
- ngModule.run(($injector) => {
- registerVisualization({
- type: 'COUNTER',
- name: 'Counter',
- getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }),
- Renderer: angular2react('counterRenderer', CounterRenderer, $injector),
- Editor: angular2react('counterEditor', CounterEditor, $injector),
-
- defaultColumns: 2,
- defaultRows: 5,
- });
+ defaultColumns: 2,
+ defaultRows: 5,
});
}
diff --git a/client/app/visualizations/counter/render.less b/client/app/visualizations/counter/render.less
new file mode 100755
index 0000000000..252d0c0242
--- /dev/null
+++ b/client/app/visualizations/counter/render.less
@@ -0,0 +1,46 @@
+.counter-visualization-container {
+ display: block;
+ text-align: center;
+ padding: 15px 10px;
+ overflow: hidden;
+
+ .counter-visualization-content {
+ margin: 0;
+ padding: 0;
+ font-size: 80px;
+ line-height: normal;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .counter-visualization-value,
+ .counter-visualization-target {
+ font-size: 1em;
+ display: block;
+ }
+
+ .counter-visualization-label {
+ font-size: 0.5em;
+ display: block;
+ }
+
+ .counter-visualization-target {
+ color: #ccc;
+ }
+
+ .counter-visualization-label {
+ font-size: 0.5em;
+ display: block;
+ }
+ }
+
+ &.trend-positive .counter-visualization-value {
+ color: #5cb85c;
+ }
+
+ &.trend-negative .counter-visualization-value {
+ color: #d9534f;
+ }
+}
diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js
new file mode 100644
index 0000000000..8a4bd1b0f6
--- /dev/null
+++ b/client/app/visualizations/counter/utils.js
@@ -0,0 +1,141 @@
+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) {
+ const rowNumber = getRowNumber(options.rowNumber, rowsCount);
+ const targetRowNumber = getRowNumber(options.targetRowNumber, rowsCount);
+ const counterColName = options.counterColName;
+ const targetColName = options.targetColName;
+ const counterLabel = options.counterLabel;
+
+ if (counterLabel) {
+ result.counterLabel = counterLabel;
+ } else {
+ result.counterLabel = visualizationName;
+ }
+
+ if (options.countRow) {
+ result.counterValue = rowsCount;
+ } else if (counterColName) {
+ result.counterValue = rows[rowNumber][counterColName];
+ }
+
+ result.showTrend = false;
+ if (targetColName) {
+ 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/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js
new file mode 100644
index 0000000000..dadfa68f49
--- /dev/null
+++ b/client/cypress/integration/visualizations/counter_spec.js
@@ -0,0 +1,207 @@
+/* global cy, Cypress */
+
+import { createQuery } from '../../support/redash-api';
+
+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
+`;
+
+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 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
+
+ Counter.EditorTabs.Formatting
+ `);
+
+ 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 with target value formatting', () => {
+ cy.clickThrough(`
+ NewVisualization
+ VisualizationType
+ VisualizationType.COUNTER
+
+ Counter.General.ValueColumn
+ Counter.General.ValueColumn.a
+
+ Counter.General.TargetValueColumn
+ Counter.General.TargetValueColumn.b
+
+ Counter.EditorTabs.Formatting
+ 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] });
+ });
+});