From 54c5011544d647e006294b9fe0c4d38a2178607d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 23 Nov 2019 19:32:57 -0300 Subject: [PATCH 1/9] Abstract Counter visualization Types --- .../counter/Editor/GeneralSettings.jsx | 150 ++++++++++-------- client/app/visualizations/counter/index.js | 1 + client/app/visualizations/counter/utils.js | 28 ++++ 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.jsx index 554a588bc9..d6634afb67 100644 --- a/client/app/visualizations/counter/Editor/GeneralSettings.jsx +++ b/client/app/visualizations/counter/Editor/GeneralSettings.jsx @@ -1,13 +1,14 @@ -import { map } from 'lodash'; +import { map, keys, includes, get } from 'lodash'; import React from 'react'; import * as Grid from 'antd/lib/grid'; import Select from 'antd/lib/select'; import Input from 'antd/lib/input'; import InputNumber from 'antd/lib/input-number'; -import Switch from 'antd/lib/switch'; import { EditorPropTypes } from '@/visualizations'; +import { COUNTER_TYPES } from '../utils'; export default function GeneralSettings({ options, data, visualizationName, onOptionsChange }) { + const showOptionForType = option => includes(get(COUNTER_TYPES[options.counterType], 'options'), option); return ( @@ -28,84 +29,101 @@ export default function GeneralSettings({ options, data, visualizationName, onOp - + - - - - - - onOptionsChange({ rowNumber })} - /> - - + {showOptionForType('counterColName') && ( + + + + + + + + + )} - - - - - - - - + {showOptionForType('rowNumber') && ( + + + + + + onOptionsChange({ rowNumber })} + /> + + + )} - - - - - - onOptionsChange({ targetRowNumber })} - /> - - + {showOptionForType('targetColName') && ( + + + + + + + + + )} - + {showOptionForType('targetRowNumber') && ( + + + + + + onOptionsChange({ targetRowNumber })} + /> + + + )} ); } diff --git a/client/app/visualizations/counter/index.js b/client/app/visualizations/counter/index.js index 07bd18af5d..b3279c9351 100644 --- a/client/app/visualizations/counter/index.js +++ b/client/app/visualizations/counter/index.js @@ -4,6 +4,7 @@ import Renderer from './Renderer'; import Editor from './Editor'; const DEFAULT_OPTIONS = { + counterType: 'rowValue', counterLabel: '', counterColName: 'counter', rowNumber: 1, diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js index 8a4bd1b0f6..77e0612392 100644 --- a/client/app/visualizations/counter/utils.js +++ b/client/app/visualizations/counter/utils.js @@ -1,6 +1,34 @@ import { isNumber, isFinite, toString } from 'lodash'; import numeral from 'numeral'; +export const COUNTER_TYPES = { + rowValue: { + name: 'Row Value', + getValue: () => 0, + options: ['counterColName', 'rowNumber', 'targetColName', 'targetRowNumber'], + }, + countRows: { + name: 'Count Rows', + getValue: () => 0, + options: ['targetColName', 'targetRowNumber'], + }, + sumRows: { + name: 'Sum Values', + getValue: () => 0, + options: ['counterColName', 'targetColName', 'targetRowNumber'], + }, + minValue: { + name: 'Min Value', + getValue: () => 0, + options: ['counterColName', 'targetColName', 'targetRowNumber'], + }, + maxValue: { + name: 'Max Value', + getValue: () => 0, + options: ['counterColName', 'targetColName', 'targetRowNumber'], + }, +}; + // 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) From 982527f677c4debf078a0f088706fbbc519cbb3f Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 25 Nov 2019 11:35:55 -0300 Subject: [PATCH 2/9] Create Counter Type migration --- ...98215_add_type_to_counter_visualization.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 migrations/versions/c0cbaae98215_add_type_to_counter_visualization.py 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))) From 54492ba6eee52081e9774acca204f9d86db83a50 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 25 Nov 2019 21:18:18 -0300 Subject: [PATCH 3/9] Add aggregate computation functions --- client/app/visualizations/counter/utils.js | 44 +++++++--------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js index 77e0612392..b141e5e857 100644 --- a/client/app/visualizations/counter/utils.js +++ b/client/app/visualizations/counter/utils.js @@ -1,30 +1,30 @@ -import { isNumber, isFinite, toString } from 'lodash'; +import { isNumber, isFinite, toString, invoke, nth, get, sumBy, map, min, max } from 'lodash'; import numeral from 'numeral'; export const COUNTER_TYPES = { rowValue: { name: 'Row Value', - getValue: () => 0, + getValue: (rows, { rowNumber, counterColName }) => get(nth(rows, rowNumber), counterColName), options: ['counterColName', 'rowNumber', 'targetColName', 'targetRowNumber'], }, countRows: { name: 'Count Rows', - getValue: () => 0, + getValue: rows => rows.length, options: ['targetColName', 'targetRowNumber'], }, sumRows: { name: 'Sum Values', - getValue: () => 0, + getValue: (rows, { counterColName }) => sumBy(rows, counterColName), options: ['counterColName', 'targetColName', 'targetRowNumber'], }, minValue: { name: 'Min Value', - getValue: () => 0, + getValue: (rows, { counterColName }) => min(map(rows, row => get(row, counterColName))), options: ['counterColName', 'targetColName', 'targetRowNumber'], }, maxValue: { name: 'Max Value', - getValue: () => 0, + getValue: (rows, { counterColName }) => max(map(rows, row => get(row, counterColName))), options: ['counterColName', 'targetColName', 'targetRowNumber'], }, }; @@ -71,18 +71,6 @@ function numberFormat(value, decimalPoints, decimalDelimiter, thousandsDelimiter 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); @@ -103,11 +91,7 @@ export function getCounterData(rows, options, visualizationName) { 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; + const { counterType, counterLabel, targetRowNumber, targetColName } = options; if (counterLabel) { result.counterLabel = counterLabel; @@ -115,15 +99,14 @@ export function getCounterData(rows, options, visualizationName) { result.counterLabel = visualizationName; } - if (options.countRow) { - result.counterValue = rowsCount; - } else if (counterColName) { - result.counterValue = rows[rowNumber][counterColName]; + const counterValue = invoke(COUNTER_TYPES[counterType], 'getValue', rows, options); + if (counterValue !== null && counterValue !== undefined) { + result.counterValue = counterValue; } result.showTrend = false; if (targetColName) { - result.targetValue = rows[targetRowNumber][targetColName]; + result.targetValue = get(nth(rows, targetRowNumber), targetColName); if (Number.isFinite(result.counterValue) && isFinite(result.targetValue)) { const delta = result.counterValue - result.targetValue; @@ -158,10 +141,9 @@ export function isValueNumber(rows, options) { const rowsCount = rows.length; if (rowsCount > 0) { - const rowNumber = getRowNumber(options.rowNumber, rowsCount); - const counterColName = options.counterColName; + const { rowNumber, counterColName } = options; if (counterColName) { - return isNumber(rows[rowNumber][counterColName]); + return isNumber(get(nth(rows, rowNumber), counterColName)); } } From 277318c88053349b147bfca13e5d5dce5cffcd23 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 6 Feb 2020 10:22:57 -0300 Subject: [PATCH 4/9] Fix wrong option name --- client/app/visualizations/counter/Editor/GeneralSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.jsx index f190a7c636..b46419d804 100644 --- a/client/app/visualizations/counter/Editor/GeneralSettings.jsx +++ b/client/app/visualizations/counter/Editor/GeneralSettings.jsx @@ -87,7 +87,7 @@ export default function GeneralSettings({ options, data, visualizationName, onOp )} - {showOptionForType("TargetValueRowNumber") && ( + {showOptionForType("targetRowNumber") && (
Date: Thu, 6 Feb 2020 11:16:31 -0300 Subject: [PATCH 5/9] Fix getting wrong row number --- client/app/visualizations/counter/utils.js | 19 +++++++++++++++---- .../app/visualizations/counter/utils.test.js | 8 ++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js index 3b49b7e1fe..97839f537e 100644 --- a/client/app/visualizations/counter/utils.js +++ b/client/app/visualizations/counter/utils.js @@ -4,7 +4,7 @@ import numeral from "numeral"; export const COUNTER_TYPES = { rowValue: { name: "Row Value", - getValue: (rows, { rowNumber, counterColName }) => get(nth(rows, rowNumber), counterColName), + getValue: (rows, { rowNumber, counterColName }) => getCellValue(rows, rowNumber, counterColName), options: ["counterColName", "rowNumber", "targetColName", "targetRowNumber"], }, countRows: { @@ -67,6 +67,17 @@ function numberFormat(value, decimalPoints, decimalDelimiter, thousandsDelimiter 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 getCellValue(rows, index, columnName) { + index = parseInt(index, 10) || 0; + if (index > 0) { + index = index - 1; + } + return get(nth(rows, index), columnName); +} + function formatValue(value, { stringPrefix, stringSuffix, stringDecimal, stringDecChar, stringThouSep }) { if (isNumber(value)) { value = numberFormat(value, stringDecimal, stringDecChar, stringThouSep); @@ -85,7 +96,7 @@ function formatTooltip(value, formatString) { export function getCounterData(rows, options, visualizationName) { const result = {}; const rowsCount = rows.length; - const { counterType, counterLabel, targetRowNumber, targetColName } = options; + const { counterType = "rowValue", counterLabel, targetRowNumber, targetColName } = options; if (rowsCount > 0 || counterType === "countRows") { result.counterLabel = counterLabel || visualizationName; @@ -98,7 +109,7 @@ export function getCounterData(rows, options, visualizationName) { result.showTrend = false; if (targetColName) { - result.targetValue = get(nth(rows, targetRowNumber), targetColName); + result.targetValue = getCellValue(rows, targetRowNumber, targetColName); if (Number.isFinite(result.counterValue) && isFinite(result.targetValue)) { const delta = result.counterValue - result.targetValue; @@ -135,7 +146,7 @@ export function isValueNumber(rows, options) { if (rowsCount > 0) { const { rowNumber, counterColName } = options; if (counterColName) { - return isNumber(get(nth(rows, rowNumber), counterColName)); + return isNumber(getCellValue(rows, rowNumber, counterColName)); } } diff --git a/client/app/visualizations/counter/utils.test.js b/client/app/visualizations/counter/utils.test.js index 561d4162eb..54921f7f57 100644 --- a/client/app/visualizations/counter/utils.test.js +++ b/client/app/visualizations/counter/utils.test.js @@ -119,7 +119,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test("Rows are counted correctly", () => { - const result = getCounterData(dummy.rows, { countRow: true }, dummy.visualisationName); + const result = getCounterData(dummy.rows, { counterType: "countRows" }, dummy.visualisationName); expect(result).toEqual(dummy.result); }); @@ -127,7 +127,7 @@ describe("Visualizations -> Counter -> Utils", () => { const result = getCounterData( dummy.rows, { - countRow: true, + counterType: "countRows", rowNumber: 3, counterColName: "population", }, @@ -140,7 +140,7 @@ describe("Visualizations -> Counter -> Utils", () => { const result = getCounterData( dummy.rows, { - countRow: true, + counterType: "countRows", targetRowNumber: 2, targetColName: "population", }, @@ -156,7 +156,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test("Empty rows return counter value 0", () => { - const result = getCounterData([], { countRow: true }, dummy.visualisationName); + const result = getCounterData([], { counterType: "countRows" }, dummy.visualisationName); expect(result).toEqual({ ...dummy.result, counterValue: "0.000", From 985edc69b2ba702d7d76f09034486929781dd5a8 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Fri, 7 Feb 2020 13:45:39 -0300 Subject: [PATCH 6/9] Update utils.test.js - added Sum/Min/Max - changed visualisation -> visualization for consistence with the rest --- .../app/visualizations/counter/utils.test.js | 81 +++++++++++++++---- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/client/app/visualizations/counter/utils.test.js b/client/app/visualizations/counter/utils.test.js index 54921f7f57..529f629013 100644 --- a/client/app/visualizations/counter/utils.test.js +++ b/client/app/visualizations/counter/utils.test.js @@ -11,9 +11,9 @@ describe("Visualizations -> Counter -> Utils", () => { { city: "Tokyo", population: 38140000 }, ], options: {}, - visualisationName: "Visualisation Name", + visualizationName: "Visualization Name", result: { - counterLabel: "Visualisation Name", + counterLabel: "Visualization Name", counterValue: "", targetValue: null, counterValueTooltip: "", @@ -23,9 +23,9 @@ describe("Visualizations -> Counter -> Utils", () => { }); describe("getCounterData()", () => { - describe('"Count rows" option is disabled', () => { + describe('Counter type is not "Count Rows"', () => { test("No target and counter values return empty result", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualisationName); + const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); expect(result).toEqual({ ...dummy.result, showTrend: false, @@ -33,7 +33,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test('"Counter label" overrides vizualization name', () => { - const result = getCounterData(dummy.rows, { counterLabel: "Counter Label" }, dummy.visualisationName); + const result = getCounterData(dummy.rows, { counterLabel: "Counter Label" }, dummy.visualizationName); expect(result).toEqual({ ...dummy.result, counterLabel: "Counter Label", @@ -42,13 +42,13 @@ describe("Visualizations -> Counter -> Utils", () => { }); test('"Counter Value Column Name" must be set to a correct non empty value', () => { - const result = getCounterData(dummy.rows, { rowNumber: 3 }, dummy.visualisationName); + const result = getCounterData(dummy.rows, { rowNumber: 3 }, dummy.visualizationName); expect(result).toEqual({ ...dummy.result, showTrend: false, }); - const result2 = getCounterData(dummy.rows, { counterColName: "missingColumn" }, dummy.visualisationName); + const result2 = getCounterData(dummy.rows, { counterColName: "missingColumn" }, dummy.visualizationName); expect(result2).toEqual({ ...dummy.result, showTrend: false, @@ -56,7 +56,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test('"Counter Value Column Name" uses correct column', () => { - const result = getCounterData(dummy.rows, { counterColName: "population" }, dummy.visualisationName); + const result = getCounterData(dummy.rows, { counterColName: "population" }, dummy.visualizationName); expect(result).toEqual({ ...dummy.result, counterValue: "18,604,000.000", @@ -74,7 +74,7 @@ describe("Visualizations -> Counter -> Utils", () => { targetRowNumber: 2, targetColName: "population", }, - dummy.visualisationName + dummy.visualizationName ); expect(result).toEqual({ ...dummy.result, @@ -94,7 +94,7 @@ describe("Visualizations -> Counter -> Utils", () => { targetRowNumber: 1, targetColName: "population", }, - dummy.visualisationName + dummy.visualizationName ); expect(result2).toEqual({ ...dummy.result, @@ -108,7 +108,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); }); - describe('"Count rows" option is enabled', () => { + describe('Counter type is "Count Rows"', () => { beforeEach(() => { dummy.result = { ...dummy.result, @@ -119,7 +119,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test("Rows are counted correctly", () => { - const result = getCounterData(dummy.rows, { counterType: "countRows" }, dummy.visualisationName); + const result = getCounterData(dummy.rows, { counterType: "countRows" }, dummy.visualizationName); expect(result).toEqual(dummy.result); }); @@ -131,7 +131,7 @@ describe("Visualizations -> Counter -> Utils", () => { rowNumber: 3, counterColName: "population", }, - dummy.visualisationName + dummy.visualizationName ); expect(result).toEqual(dummy.result); }); @@ -144,7 +144,7 @@ describe("Visualizations -> Counter -> Utils", () => { targetRowNumber: 2, targetColName: "population", }, - dummy.visualisationName + dummy.visualizationName ); expect(result).toEqual({ ...dummy.result, @@ -156,7 +156,7 @@ describe("Visualizations -> Counter -> Utils", () => { }); test("Empty rows return counter value 0", () => { - const result = getCounterData([], { counterType: "countRows" }, dummy.visualisationName); + const result = getCounterData([], { counterType: "countRows" }, dummy.visualizationName); expect(result).toEqual({ ...dummy.result, counterValue: "0.000", @@ -164,5 +164,56 @@ describe("Visualizations -> Counter -> Utils", () => { }); }); }); + + describe('Counter type is "Sum Values"', () => { + beforeEach(() => { + dummy.options = { counterType: "sumRows", counterColName: "population" }; + dummy.result = { + ...dummy.result, + counterValue: "81,228,000.000", + counterValueTooltip: "81,228,000", + showTrend: false, + }; + }); + + test("Rows are summed up correctly", () => { + const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); + expect(result).toEqual(dummy.result); + }); + }); + + describe('Counter type is "Min Value"', () => { + beforeEach(() => { + dummy.options = { counterType: "minValue", counterColName: "population" }; + dummy.result = { + ...dummy.result, + counterValue: "18,604,000.000", + counterValueTooltip: "18,604,000", + showTrend: false, + }; + }); + + test("The min value from rows is returned", () => { + const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); + expect(result).toEqual(dummy.result); + }); + }); + + describe('Counter type is "Max Value"', () => { + beforeEach(() => { + dummy.options = { counterType: "maxValue", counterColName: "population" }; + dummy.result = { + ...dummy.result, + counterValue: "38,140,000.000", + counterValueTooltip: "38,140,000", + showTrend: false, + }; + }); + + test("The max value from rows is returned", () => { + const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); + expect(result).toEqual(dummy.result); + }); + }); }); }); From b06e4230a61db679b77091c73dcb543a078226aa Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Fri, 7 Feb 2020 16:25:37 -0300 Subject: [PATCH 7/9] Fix Cypress counter_spec --- client/cypress/integration/visualizations/counter_spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js index b52f8454f4..879a4bfaa2 100644 --- a/client/cypress/integration/visualizations/counter_spec.js +++ b/client/cypress/integration/visualizations/counter_spec.js @@ -141,10 +141,8 @@ describe("Counter", () => { VisualizationType VisualizationType.COUNTER - Counter.General.ValueColumn - Counter.General.ValueColumn.a - - Counter.General.CountRows + Counter.General.Type + Counter.General.Type.countRows `); cy.getByTestId("VisualizationPreview") From 394c931e8232d2860c4b5dc0afa650a89d7bac13 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Thu, 12 Mar 2020 11:27:33 +0200 Subject: [PATCH 8/9] Counter enhancements (#4642) * Counter enhancements * Code style * Allow to deselect column; don't migrate options for new visualizations; minor fixes * Secondary values editor: disable input when secondary value not available * Use row data (if available) when formatting values and tooltips * Show secondary value controls only visibility - don't disable inputs * Mve round baces around secondary value to format templates * Fix options migration bug * Render counter only if some data available * Ability to disable secondary value; place Show Secondary Value checkbox after other options * Format hints * Fix options migration bug * Tests: getCounterData * Tests: Editor * Code style * Tests: Renderer * Tests: Renderer (unit-tests) * Fix unit tests hanging on CI * CR1 --- .circleci/config.yml | 2 +- .../counter/Editor/CounterValueOptions.jsx | 142 ++ .../counter/Editor/FormatSettings.jsx | 83 - .../counter/Editor/GeneralSettings.jsx | 112 +- .../counter/Editor/GeneralSettings.test.jsx | 62 + .../counter/Editor/PrimaryValueSettings.jsx | 20 + .../Editor/PrimaryValueSettings.test.jsx | 81 + .../counter/Editor/SecondaryValueSettings.jsx | 34 + .../Editor/SecondaryValueSettings.test.jsx | 90 ++ .../GeneralSettings.test.jsx.snap | 25 + .../PrimaryValueSettings.test.jsx.snap | 49 + .../SecondaryValueSettings.test.jsx.snap | 57 + .../visualizations/counter/Editor/index.js | 6 +- .../app/visualizations/counter/Renderer.jsx | 56 +- .../visualizations/counter/Renderer.test.jsx | 188 +++ .../__snapshots__/Renderer.test.jsx.snap | 1345 +++++++++++++++++ .../visualizations/counter/counterTypes.js | 63 + .../counter-types/countRows/default.json | 49 + .../counter-types/maxValue/default.json | 49 + .../maxValue/invalid-column.json | 49 + .../counter-types/minValue/default.json | 49 + .../minValue/invalid-column.json | 49 + .../counter-types/rowValue/default.json | 49 + .../rowValue/invalid-column.json | 49 + .../rowValue/negative-row-number.json | 49 + .../rowValue/positive-row-number.json | 49 + .../rowValue/wrapped-negative-row-number.json | 49 + .../rowValue/wrapped-positive-row-number.json | 49 + .../rowValue/zero-row-number.json | 49 + .../counter-types/sumRows/default.json | 49 + .../counter-types/sumRows/invalid-column.json | 49 + .../counter-types/unused/default.json | 49 + .../display-and-tooltip-format.json | 50 + .../fixtures/getCounterData/hide-tooltip.json | 50 + .../primary-secondary-different-types.json | 50 + .../getCounterData/trend-negative.json | 50 + .../getCounterData/trend-positive.json | 50 + .../uses-custom-counter-label.json | 38 + .../uses-custom-number-format.json | 50 + .../uses-default-counter-label.json | 38 + .../visualizations/counter/getCounterData.js | 72 + .../counter/getCounterData.test.js | 166 ++ .../counter/getOptions/index.js | 3 + .../visualizations/counter/getOptions/v1.js | 22 + .../visualizations/counter/getOptions/v2.js | 76 + client/app/visualizations/counter/index.js | 15 +- client/app/visualizations/counter/render.less | 8 + client/app/visualizations/counter/utils.js | 154 -- .../app/visualizations/counter/utils.test.js | 219 --- .../visualizations/counter_spec.js | 328 ++-- 50 files changed, 3817 insertions(+), 772 deletions(-) create mode 100644 client/app/visualizations/counter/Editor/CounterValueOptions.jsx delete mode 100644 client/app/visualizations/counter/Editor/FormatSettings.jsx create mode 100644 client/app/visualizations/counter/Editor/GeneralSettings.test.jsx create mode 100644 client/app/visualizations/counter/Editor/PrimaryValueSettings.jsx create mode 100644 client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx create mode 100644 client/app/visualizations/counter/Editor/SecondaryValueSettings.jsx create mode 100644 client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx create mode 100644 client/app/visualizations/counter/Editor/__snapshots__/GeneralSettings.test.jsx.snap create mode 100644 client/app/visualizations/counter/Editor/__snapshots__/PrimaryValueSettings.test.jsx.snap create mode 100644 client/app/visualizations/counter/Editor/__snapshots__/SecondaryValueSettings.test.jsx.snap create mode 100644 client/app/visualizations/counter/Renderer.test.jsx create mode 100644 client/app/visualizations/counter/__snapshots__/Renderer.test.jsx.snap create mode 100644 client/app/visualizations/counter/counterTypes.js create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/countRows/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/maxValue/invalid-column.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/minValue/invalid-column.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/invalid-column.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/negative-row-number.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/positive-row-number.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-negative-row-number.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/wrapped-positive-row-number.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/rowValue/zero-row-number.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/sumRows/invalid-column.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/counter-types/unused/default.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/display-and-tooltip-format.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/hide-tooltip.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/primary-secondary-different-types.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/trend-negative.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/trend-positive.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/uses-custom-counter-label.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/uses-custom-number-format.json create mode 100644 client/app/visualizations/counter/fixtures/getCounterData/uses-default-counter-label.json create mode 100644 client/app/visualizations/counter/getCounterData.js create mode 100644 client/app/visualizations/counter/getCounterData.test.js create mode 100644 client/app/visualizations/counter/getOptions/index.js create mode 100644 client/app/visualizations/counter/getOptions/v1.js create mode 100644 client/app/visualizations/counter/getOptions/v2.js delete mode 100644 client/app/visualizations/counter/utils.js delete mode 100644 client/app/visualizations/counter/utils.test.js diff --git a/.circleci/config.yml b/.circleci/config.yml index a84872890f..3ad7700c01 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -74,7 +74,7 @@ jobs: - run: sudo pip3 install -r requirements_bundles.txt - run: npm install - 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 b46419d804..60226892be 100644 --- a/client/app/visualizations/counter/Editor/GeneralSettings.jsx +++ b/client/app/visualizations/counter/Editor/GeneralSettings.jsx @@ -1,11 +1,8 @@ -import { map, keys, includes, get } from "lodash"; import React from "react"; -import { Section, Select, Input, InputNumber } from "@/components/visualizations/editor"; +import { Section, Input, ContextHelp } from "@/components/visualizations/editor"; import { EditorPropTypes } from "@/visualizations/prop-types"; -import { COUNTER_TYPES } from "../utils"; -export default function GeneralSettings({ options, data, visualizationName, onOptionsChange }) { - const showOptionForType = option => includes(get(COUNTER_TYPES[options.counterType], "options"), option); +export default function GeneralSettings({ options, visualizationName, onOptionsChange }) { return (
@@ -13,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 })} @@ -21,84 +18,41 @@ export default function GeneralSettings({ options, data, visualizationName, onOp
- + data-test="Counter.NumberFormat" + defaultValue={options.numberFormat} + onChange={e => onOptionsChange({ numberFormat: e.target.value })} + />
- {showOptionForType("counterColName") && ( -
- -
- )} - - {showOptionForType("rowNumber") && ( -
- onOptionsChange({ rowNumber })} - /> -
- )} - - {showOptionForType("targetColName") && ( -
- -
- )} +
+ onOptionsChange({ stringDecChar: e.target.value })} + /> +
- {showOptionForType("targetRowNumber") && ( -
- onOptionsChange({ targetRowNumber })} - /> -
- )} +
+ onOptionsChange({ stringThouSep: e.target.value })} + /> +
); } 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..656d4896e2 --- /dev/null +++ b/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx @@ -0,0 +1,62 @@ +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) { + options = getOptions(options); + 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..152acd9f5b --- /dev/null +++ b/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx @@ -0,0 +1,81 @@ +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) { + options = getOptions(options); + 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..3a0b13943a --- /dev/null +++ b/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx @@ -0,0 +1,90 @@ +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) { + options = getOptions(options); + 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..f64b0d9220 --- /dev/null +++ b/client/app/visualizations/counter/Renderer.test.jsx @@ -0,0 +1,188 @@ +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "./getOptions"; +import Renderer from "./Renderer"; + +function mount(options) { + options = getOptions(options); + 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..3c705605fc --- /dev/null +++ b/client/app/visualizations/counter/getOptions/v2.js @@ -0,0 +1,76 @@ +import { isEmpty, isFinite, merge, get, pick, padEnd, toString } 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) { + const currentSchemaVersion = get(options, "schemaVersion", isEmpty(options) ? schemaVersion : 0); + if (currentSchemaVersion < schemaVersion) { + options = migrateFromV1(options); + } + return merge({}, defaultOptions, options, { schemaVersion }); +} diff --git a/client/app/visualizations/counter/index.js b/client/app/visualizations/counter/index.js index 8d439f51ed..18e95a07c2 100644 --- a/client/app/visualizations/counter/index.js +++ b/client/app/visualizations/counter/index.js @@ -1,22 +1,11 @@ import Renderer from "./Renderer"; import Editor from "./Editor"; - -const DEFAULT_OPTIONS = { - counterType: "rowValue", - 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 97839f537e..0000000000 --- a/client/app/visualizations/counter/utils.js +++ /dev/null @@ -1,154 +0,0 @@ -import { isNumber, isFinite, toString, invoke, nth, get, sumBy, map, min, max } from "lodash"; -import numeral from "numeral"; - -export const COUNTER_TYPES = { - rowValue: { - name: "Row Value", - getValue: (rows, { rowNumber, counterColName }) => getCellValue(rows, rowNumber, counterColName), - options: ["counterColName", "rowNumber", "targetColName", "targetRowNumber"], - }, - countRows: { - name: "Count Rows", - getValue: rows => rows.length, - options: ["targetColName", "targetRowNumber"], - }, - sumRows: { - name: "Sum Values", - getValue: (rows, { counterColName }) => sumBy(rows, counterColName), - options: ["counterColName", "targetColName", "targetRowNumber"], - }, - minValue: { - name: "Min Value", - getValue: (rows, { counterColName }) => min(map(rows, row => get(row, counterColName))), - options: ["counterColName", "targetColName", "targetRowNumber"], - }, - maxValue: { - name: "Max Value", - getValue: (rows, { counterColName }) => max(map(rows, row => get(row, counterColName))), - options: ["counterColName", "targetColName", "targetRowNumber"], - }, -}; - -// 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 getCellValue(rows, index, columnName) { - index = parseInt(index, 10) || 0; - if (index > 0) { - index = index - 1; - } - return get(nth(rows, index), columnName); -} - -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; - const { counterType = "rowValue", counterLabel, targetRowNumber, targetColName } = options; - - if (rowsCount > 0 || counterType === "countRows") { - result.counterLabel = counterLabel || visualizationName; - - const counterValue = invoke(COUNTER_TYPES[counterType], "getValue", rows, options); - if (counterValue !== null && counterValue !== undefined) { - result.counterValue = counterValue; - } - - result.showTrend = false; - - if (targetColName) { - result.targetValue = getCellValue(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.counterType === "countRows") { - return true; // array length is always a number - } - - const rowsCount = rows.length; - if (rowsCount > 0) { - const { rowNumber, counterColName } = options; - if (counterColName) { - return isNumber(getCellValue(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 529f629013..0000000000 --- a/client/app/visualizations/counter/utils.test.js +++ /dev/null @@ -1,219 +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: {}, - visualizationName: "Visualization Name", - result: { - counterLabel: "Visualization Name", - counterValue: "", - targetValue: null, - counterValueTooltip: "", - targetValueTooltip: "", - }, - }; - }); - - describe("getCounterData()", () => { - describe('Counter type is not "Count Rows"', () => { - test("No target and counter values return empty result", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); - expect(result).toEqual({ - ...dummy.result, - showTrend: false, - }); - }); - - test('"Counter label" overrides vizualization name', () => { - const result = getCounterData(dummy.rows, { counterLabel: "Counter Label" }, dummy.visualizationName); - 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.visualizationName); - expect(result).toEqual({ - ...dummy.result, - showTrend: false, - }); - - const result2 = getCounterData(dummy.rows, { counterColName: "missingColumn" }, dummy.visualizationName); - expect(result2).toEqual({ - ...dummy.result, - showTrend: false, - }); - }); - - test('"Counter Value Column Name" uses correct column', () => { - const result = getCounterData(dummy.rows, { counterColName: "population" }, dummy.visualizationName); - 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.visualizationName - ); - 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.visualizationName - ); - 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('Counter type is "Count Rows"', () => { - beforeEach(() => { - dummy.result = { - ...dummy.result, - counterValue: "3.000", - counterValueTooltip: "3", - showTrend: false, - }; - }); - - test("Rows are counted correctly", () => { - const result = getCounterData(dummy.rows, { counterType: "countRows" }, dummy.visualizationName); - expect(result).toEqual(dummy.result); - }); - - test("Counter value is ignored", () => { - const result = getCounterData( - dummy.rows, - { - counterType: "countRows", - rowNumber: 3, - counterColName: "population", - }, - dummy.visualizationName - ); - expect(result).toEqual(dummy.result); - }); - - test("Target value and trend are computed correctly", () => { - const result = getCounterData( - dummy.rows, - { - counterType: "countRows", - targetRowNumber: 2, - targetColName: "population", - }, - dummy.visualizationName - ); - 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([], { counterType: "countRows" }, dummy.visualizationName); - expect(result).toEqual({ - ...dummy.result, - counterValue: "0.000", - counterValueTooltip: "0", - }); - }); - }); - - describe('Counter type is "Sum Values"', () => { - beforeEach(() => { - dummy.options = { counterType: "sumRows", counterColName: "population" }; - dummy.result = { - ...dummy.result, - counterValue: "81,228,000.000", - counterValueTooltip: "81,228,000", - showTrend: false, - }; - }); - - test("Rows are summed up correctly", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); - expect(result).toEqual(dummy.result); - }); - }); - - describe('Counter type is "Min Value"', () => { - beforeEach(() => { - dummy.options = { counterType: "minValue", counterColName: "population" }; - dummy.result = { - ...dummy.result, - counterValue: "18,604,000.000", - counterValueTooltip: "18,604,000", - showTrend: false, - }; - }); - - test("The min value from rows is returned", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); - expect(result).toEqual(dummy.result); - }); - }); - - describe('Counter type is "Max Value"', () => { - beforeEach(() => { - dummy.options = { counterType: "maxValue", counterColName: "population" }; - dummy.result = { - ...dummy.result, - counterValue: "38,140,000.000", - counterValueTooltip: "38,140,000", - showTrend: false, - }; - }); - - test("The max value from rows is returned", () => { - const result = getCounterData(dummy.rows, dummy.options, dummy.visualizationName); - expect(result).toEqual(dummy.result); - }); - }); - }); -}); diff --git a/client/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js index 879a4bfaa2..257c9c2598 100644 --- a/client/cypress/integration/visualizations/counter_spec.js +++ b/client/cypress/integration/visualizations/counter_spec.js @@ -1,221 +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 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 (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 count rows", () => { - cy.clickThrough(` - NewVisualization - VisualizationType - VisualizationType.COUNTER - - Counter.General.Type - Counter.General.Type.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] }); + }); }); }); From 2a3f66d6086756abd5e547624aa23d79adb37709 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 24 Mar 2020 10:49:25 -0300 Subject: [PATCH 9/9] Counter: Only autoselect existing columns in data (#4738) --- .../counter/Editor/GeneralSettings.test.jsx | 18 +++++----- .../Editor/PrimaryValueSettings.test.jsx | 18 +++++----- .../Editor/SecondaryValueSettings.test.jsx | 18 +++++----- .../visualizations/counter/Renderer.test.jsx | 33 ++++++++----------- .../visualizations/counter/getOptions/v2.js | 18 ++++++++-- 5 files changed, 59 insertions(+), 46 deletions(-) diff --git a/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx b/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx index 656d4896e2..ec152a8888 100644 --- a/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx +++ b/client/app/visualizations/counter/Editor/GeneralSettings.test.jsx @@ -10,17 +10,19 @@ function findByTestID(wrapper, testId) { } function mount(options, done) { - options = getOptions(options); + 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(); diff --git a/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx b/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx index 152acd9f5b..26a101867e 100644 --- a/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx +++ b/client/app/visualizations/counter/Editor/PrimaryValueSettings.test.jsx @@ -10,17 +10,19 @@ function findByTestID(wrapper, testId) { } function mount(options, done) { - options = getOptions(options); + 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(); diff --git a/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx b/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx index 3a0b13943a..1fa152e28f 100644 --- a/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx +++ b/client/app/visualizations/counter/Editor/SecondaryValueSettings.test.jsx @@ -10,17 +10,19 @@ function findByTestID(wrapper, testId) { } function mount(options, done) { - options = getOptions(options); + 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(); diff --git a/client/app/visualizations/counter/Renderer.test.jsx b/client/app/visualizations/counter/Renderer.test.jsx index f64b0d9220..64b97d964d 100644 --- a/client/app/visualizations/counter/Renderer.test.jsx +++ b/client/app/visualizations/counter/Renderer.test.jsx @@ -5,25 +5,20 @@ import getOptions from "./getOptions"; import Renderer from "./Renderer"; function mount(options) { - options = getOptions(options); - return enzyme.mount( - - ); + 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", () => { diff --git a/client/app/visualizations/counter/getOptions/v2.js b/client/app/visualizations/counter/getOptions/v2.js index 3c705605fc..5b4b692c85 100644 --- a/client/app/visualizations/counter/getOptions/v2.js +++ b/client/app/visualizations/counter/getOptions/v2.js @@ -1,4 +1,4 @@ -import { isEmpty, isFinite, merge, get, pick, padEnd, toString } from "lodash"; +import { isEmpty, isFinite, merge, get, pick, padEnd, toString, includes, map } from "lodash"; import getOptionsV1 from "./v1"; const schemaVersion = 2; @@ -67,10 +67,22 @@ function migrateFromV1(options) { return result; } -export default function getOptions(options) { +export default function getOptions(options, { columns }) { const currentSchemaVersion = get(options, "schemaVersion", isEmpty(options) ? schemaVersion : 0); if (currentSchemaVersion < schemaVersion) { options = migrateFromV1(options); } - return merge({}, defaultOptions, options, { schemaVersion }); + + 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); }