diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png index e4ef027ccb0299d31c9d968ae54c8471def03464..57ba1762ec20a73507e97b8fa83c9b51704ee0c2 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png index c9f7a1b2f3cac112e9068532c009d0dc5c5f0597..84326feca1bb046004f58f7fb185f4d8ee4cd6ed 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Transparent_Theme_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Transparent_Theme_Default.png index fe71b9d3af5215d68f9b62dbe358de8df758ac47..b20d4d7366eb638371d67e78e8effe02a2c52415 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Transparent_Theme_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Transparent_Theme_Default.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Chart.png b/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Chart.png new file mode 100644 index 0000000000000000000000000000000000000000..becfa055f272f6ed2bae49fb8d0b4c23cfcfb6b9 Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Chart.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Legend.png b/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Legend.png new file mode 100644 index 0000000000000000000000000000000000000000..f4be32705a4f05173423588f630c00233ada9cef Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_PieChart_Num_Decimal_Places_Legend.png differ diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts index e033e14fb98c5586f3ca4e95a6a530c8093ffd4b..4f49458a6bf39a7adc595323d2ba757b419578a8 100644 --- a/frontend/src/metabase-types/api/card.ts +++ b/frontend/src/metabase-types/api/card.ts @@ -196,6 +196,7 @@ export type VisualizationSettings = { "pie.show_legend"?: boolean; "pie.show_total"?: boolean; "pie.percent_visibility"?: "off" | "legend" | "inside"; + "pie.decimal_places"?: number; "pie.slice_threshold"?: number; "pie.colors"?: Record<string, string>; diff --git a/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx b/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx index e14915771a34c060695f591f09789c01ecafcc78..9e22fd4d5cb060124ba5f38f6537048bdd74ec61 100644 --- a/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/PieChart/PieChart.stories.tsx @@ -182,6 +182,20 @@ NullDimension.args = { renderingContext, }; +export const NumDecimalPlacesChart = Template.bind({}); +NumDecimalPlacesChart.args = { + rawSeries: data.numDecimalPlacesChart as any, + dashcardSettings: {}, + renderingContext, +}; + +export const NumDecimalPlacesLegend = Template.bind({}); +NumDecimalPlacesLegend.args = { + rawSeries: data.numDecimalPlacesLegend as any, + dashcardSettings: {}, + renderingContext, +}; + export const UnaggregatedDimension = Template.bind({}); UnaggregatedDimension.args = { rawSeries: data.unaggregatedDimension as any, diff --git a/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts b/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts index 59464dcd717f6fb9b25bedd1c8dd1a64faba4dc3..d88b9e0d49eee97919fb2b224ac57fe2e8e3ed12 100644 --- a/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts +++ b/frontend/src/metabase/static-viz/components/PieChart/stories-data/index.ts @@ -21,6 +21,8 @@ import missingLabelLargeSlice38424 from "./missing-label-large-slice-38424.json" import mixedPostiiveNegative from "./mixed-positive-negative.json"; import noSingleColumnLegend45149 from "./no-single-column-legend-45149.json"; import nullDimension from "./null-dimension.json"; +import numDecimalPlacesChart from "./num-decimal-places-chart.json"; +import numDecimalPlacesLegend from "./num-decimal-places-legend.json"; import numericDimension from "./numeric-dimension.json"; import numericSQLColumnCrashes28568 from "./numeric-sql-column-crashes-28568.json"; import percentagesOnChartBooleanDimensionCrashes44085 from "./percentages-on-chart-boolean-dimension-crashes-44085.json"; @@ -57,6 +59,8 @@ export const data = { dateDimension, relativeDateDimension, nullDimension, + numDecimalPlacesChart, + numDecimalPlacesLegend, unaggregatedDimension, singleDimension, longDimensionName, diff --git a/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-chart.json b/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-chart.json new file mode 100644 index 0000000000000000000000000000000000000000..da9cf3fbdc229ee0d61b4ce2c969f56cfc8ca96e --- /dev/null +++ b/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-chart.json @@ -0,0 +1,305 @@ +[ + { + "card": { + "original_card_id": 398, + "can_delete": false, + "public_uuid": null, + "parameter_usage_count": 0, + "created_at": "2024-08-01T21:22:49.134055Z", + "parameters": [], + "metabase_version": "v0.1.26-SNAPSHOT (7892978)", + "collection": { + "authority_level": null, + "description": null, + "archived": false, + "trashed_from_location": null, + "slug": "pie", + "archive_operation_id": null, + "name": "Pie", + "personal_owner_id": null, + "type": null, + "is_sample": false, + "id": 23, + "archived_directly": null, + "entity_id": "hy98llXqY3i_uwtm5ISY_", + "location": "/5/", + "namespace": null, + "is_personal": false, + "created_at": "2024-06-03T19:59:20.91934Z" + }, + "visualization_settings": { + "pie.percent_visibility": "inside", + "pie.decimal_places": 2, + "pie.slice_threshold": 2.5 + }, + "last-edit-info": { + "id": 1, + "email": "emmad@metabase.com", + "first_name": "Emmad", + "last_name": "Usmani", + "timestamp": "2024-08-01T21:22:49.387375Z" + }, + "collection_preview": true, + "entity_id": "ihJeRTVb-c1uOA9nz6mqI", + "archived_directly": false, + "display": "pie", + "parameter_mappings": [], + "id": 398, + "dataset_query": { + "database": 2, + "type": "query", + "query": { + "aggregation": [["count"]], + "breakout": [ + [ + "field", + "platform", + { + "base-type": "type/Text" + } + ] + ], + "source-table": "card__77" + } + }, + "cache_ttl": null, + "embedding_params": null, + "made_public_by_id": null, + "updated_at": "2024-08-01T21:59:42.979845Z", + "moderation_reviews": [], + "can_restore": false, + "creator_id": 1, + "average_query_time": 428.6666666666667, + "type": "question", + "last_used_at": "2024-08-01T21:59:25.328312Z", + "dashboard_count": 2, + "last_query_start": "2024-08-01T21:59:25.079201Z", + "name": "Pie - Number of Decimal Places Chart - Metacritic Games by platform", + "query_type": "query", + "collection_id": 23, + "enable_embedding": false, + "database_id": 2, + "trashed_from_collection_id": null, + "can_write": true, + "initially_published_at": null, + "creator": { + "email": "emmad@metabase.com", + "first_name": "Emmad", + "last_login": "2024-07-18T22:43:41.982476Z", + "is_qbnewb": false, + "is_superuser": true, + "id": 1, + "last_name": "Usmani", + "date_joined": "2023-11-21T21:25:41.062104Z", + "common_name": "Emmad Usmani" + }, + "result_metadata": [ + { + "semantic_type": "type/Category", + "name": "platform", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 2, + "q1": 186, + "q3": 1122, + "max": 4592, + "sd": 1015.5250832969095, + "avg": 815.6363636363636 + } + } + } + } + ], + "can_run_adhoc_query": true, + "table_id": 143, + "collection_position": null, + "view_count": 6, + "archived": false, + "description": null, + "cache_invalidated_at": null, + "displayIsLocked": true + }, + "data": { + "rows": [ + ["3DS", 400], + ["DS", 730], + ["Dreamcast", 125], + ["GameBoyAdvance", 444], + ["GameCube", 452], + ["Nintendo64", 70], + ["PC", 4592], + ["PSP", 513], + ["PlayStation", 188], + ["PlayStation2", 1418], + ["PlayStation3", 1269], + ["PlayStation4", 1928], + ["PlayStation5", 3], + ["PlayStationVita", 257], + ["Stadia", 4], + ["Switch", 1122], + ["Wii", 664], + ["WiiU", 186], + ["Xbox", 793], + ["Xbox360", 1666], + ["XboxOne", 1118], + ["XboxSeriesX", 2] + ], + "cols": [ + { + "database_type": "varchar", + "semantic_type": "type/Category", + "table_id": 143, + "name": "platform", + "source": "breakout", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "position": 2, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "database_type": "int8", + "semantic_type": "type/Quantity", + "name": "count", + "source": "aggregation", + "field_ref": ["aggregation", 0], + "effective_type": "type/BigInteger", + "aggregation_index": 0, + "display_name": "Count", + "base_type": "type/BigInteger" + } + ], + "native_form": { + "query": "SELECT \"source\".\"platform\" AS \"platform\", COUNT(*) AS \"count\" FROM (SELECT \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"id\" AS \"id\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"name\" AS \"name\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"platform\" AS \"platform\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"r_date\" AS \"r_date\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"score\" AS \"score\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"user_score\" AS \"user_score\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"developer\" AS \"developer\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"genre\" AS \"genre\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"players\" AS \"players\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"critics\" AS \"critics\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"users\" AS \"users\" FROM \"csv_upload_data\".\"csv_upload_games_20231121140652\") AS \"source\" GROUP BY \"source\".\"platform\" ORDER BY \"source\".\"platform\" ASC", + "params": null + }, + "dataset": true, + "model": true, + "format-rows?": true, + "results_timezone": "America/Los_Angeles", + "results_metadata": { + "columns": [ + { + "semantic_type": "type/Category", + "name": "platform", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 2, + "q1": 186, + "q3": 1122, + "max": 4592, + "sd": 1015.5250832969095, + "avg": 815.6363636363636 + } + } + } + } + ] + }, + "insights": null + } + } +] diff --git a/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-legend.json b/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-legend.json new file mode 100644 index 0000000000000000000000000000000000000000..0d832d7cee2a8760b9dae25cf71f2593227627e9 --- /dev/null +++ b/frontend/src/metabase/static-viz/components/PieChart/stories-data/num-decimal-places-legend.json @@ -0,0 +1,305 @@ +[ + { + "card": { + "original_card_id": 399, + "can_delete": false, + "public_uuid": null, + "parameter_usage_count": 0, + "created_at": "2024-08-01T21:23:56.967417Z", + "parameters": [], + "metabase_version": "v0.1.26-SNAPSHOT (7892978)", + "collection": { + "authority_level": null, + "description": null, + "archived": false, + "trashed_from_location": null, + "slug": "pie", + "archive_operation_id": null, + "name": "Pie", + "personal_owner_id": null, + "type": null, + "is_sample": false, + "id": 23, + "archived_directly": null, + "entity_id": "hy98llXqY3i_uwtm5ISY_", + "location": "/5/", + "namespace": null, + "is_personal": false, + "created_at": "2024-06-03T19:59:20.91934Z" + }, + "visualization_settings": { + "pie.percent_visibility": "legend", + "pie.slice_threshold": 2.5, + "pie.decimal_places": 4 + }, + "last-edit-info": { + "id": 1, + "email": "emmad@metabase.com", + "first_name": "Emmad", + "last_name": "Usmani", + "timestamp": "2024-08-01T21:23:57.177037Z" + }, + "collection_preview": true, + "entity_id": "a58QVQk3JMHDLyD3DbOfc", + "archived_directly": false, + "display": "pie", + "parameter_mappings": [], + "id": 399, + "dataset_query": { + "database": 2, + "type": "query", + "query": { + "aggregation": [["count"]], + "breakout": [ + [ + "field", + "platform", + { + "base-type": "type/Text" + } + ] + ], + "source-table": "card__77" + } + }, + "cache_ttl": null, + "embedding_params": null, + "made_public_by_id": null, + "updated_at": "2024-08-01T21:57:01.087959Z", + "moderation_reviews": [], + "can_restore": false, + "creator_id": 1, + "average_query_time": 357.6666666666667, + "type": "question", + "last_used_at": "2024-08-01T21:56:46.222032Z", + "dashboard_count": 1, + "last_query_start": "2024-08-01T21:56:45.945356Z", + "name": "Pie - Number of Decimal Places Legend - Metacritic Games by platform - Modified", + "query_type": "query", + "collection_id": 23, + "enable_embedding": false, + "database_id": 2, + "trashed_from_collection_id": null, + "can_write": true, + "initially_published_at": null, + "creator": { + "email": "emmad@metabase.com", + "first_name": "Emmad", + "last_login": "2024-07-18T22:43:41.982476Z", + "is_qbnewb": false, + "is_superuser": true, + "id": 1, + "last_name": "Usmani", + "date_joined": "2023-11-21T21:25:41.062104Z", + "common_name": "Emmad Usmani" + }, + "result_metadata": [ + { + "semantic_type": "type/Category", + "name": "platform", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 2, + "q1": 186, + "q3": 1122, + "max": 4592, + "sd": 1015.5250832969095, + "avg": 815.6363636363636 + } + } + } + } + ], + "can_run_adhoc_query": true, + "table_id": 143, + "collection_position": null, + "view_count": 3, + "archived": false, + "description": null, + "cache_invalidated_at": null, + "displayIsLocked": true + }, + "data": { + "rows": [ + ["3DS", 400], + ["DS", 730], + ["Dreamcast", 125], + ["GameBoyAdvance", 444], + ["GameCube", 452], + ["Nintendo64", 70], + ["PC", 4592], + ["PSP", 513], + ["PlayStation", 188], + ["PlayStation2", 1418], + ["PlayStation3", 1269], + ["PlayStation4", 1928], + ["PlayStation5", 3], + ["PlayStationVita", 257], + ["Stadia", 4], + ["Switch", 1122], + ["Wii", 664], + ["WiiU", 186], + ["Xbox", 793], + ["Xbox360", 1666], + ["XboxOne", 1118], + ["XboxSeriesX", 2] + ], + "cols": [ + { + "database_type": "varchar", + "semantic_type": "type/Category", + "table_id": 143, + "name": "platform", + "source": "breakout", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "position": 2, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "database_type": "int8", + "semantic_type": "type/Quantity", + "name": "count", + "source": "aggregation", + "field_ref": ["aggregation", 0], + "effective_type": "type/BigInteger", + "aggregation_index": 0, + "display_name": "Count", + "base_type": "type/BigInteger" + } + ], + "native_form": { + "query": "SELECT \"source\".\"platform\" AS \"platform\", COUNT(*) AS \"count\" FROM (SELECT \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"id\" AS \"id\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"name\" AS \"name\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"platform\" AS \"platform\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"r_date\" AS \"r_date\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"score\" AS \"score\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"user_score\" AS \"user_score\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"developer\" AS \"developer\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"genre\" AS \"genre\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"players\" AS \"players\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"critics\" AS \"critics\", \"csv_upload_data\".\"csv_upload_games_20231121140652\".\"users\" AS \"users\" FROM \"csv_upload_data\".\"csv_upload_games_20231121140652\") AS \"source\" GROUP BY \"source\".\"platform\" ORDER BY \"source\".\"platform\" ASC", + "params": null + }, + "dataset": true, + "model": true, + "format-rows?": true, + "results_timezone": "America/Los_Angeles", + "results_metadata": { + "columns": [ + { + "semantic_type": "type/Category", + "name": "platform", + "field_ref": [ + "field", + "platform", + { + "base-type": "type/Text" + } + ], + "effective_type": "type/Text", + "id": 1352, + "visibility_type": "normal", + "display_name": "Platform", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.5754 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "name": "count", + "fingerprint": { + "global": { + "distinct-count": 22, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 2, + "q1": 186, + "q3": 1122, + "max": 4592, + "sd": 1015.5250832969095, + "avg": 815.6363636363636 + } + } + } + } + ] + }, + "insights": null + } + } +] diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.tsx index 7584aa6cea60ebbeeccac3193b8dcef06360098c..f692dc2bb7ec1510ae27a2738d8516a90ef67e16 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.tsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.tsx @@ -20,15 +20,22 @@ const ALLOWED_CHARS = [ "e", ]; +// Note: there are more props than these that are provided by the viz settings +// code, we just don't have types for them here. interface ChartSettingInputProps { value: number | undefined; onChange: (value: number | undefined) => void; onChangeSettings: () => void; + options?: { + isInteger?: boolean; + isNonNegative?: boolean; + }; } export const ChartSettingInputNumeric = ({ onChange, value, + options, ...props }: ChartSettingInputProps) => { const [internalValue, setInternalValue] = useState(value?.toString() ?? ""); @@ -49,11 +56,19 @@ export const ChartSettingInputNumeric = ({ } }} onBlur={(e: React.ChangeEvent<HTMLInputElement>) => { - const num = e.target.value !== "" ? Number(e.target.value) : Number.NaN; + let num = e.target.value !== "" ? Number(e.target.value) : Number.NaN; + if (options?.isInteger) { + num = Math.round(num); + } + if (options?.isNonNegative && num < 0) { + num *= -1; + } + if (isNaN(num)) { onChange(undefined); } else { onChange(num); + setInternalValue(String(num)); } }} /> diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.unit.spec.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.unit.spec.tsx index 2ed45f1ba2878ae1e730f7a871ea790b5ea8dcf3..093ca9b0ce69efbb4112388db08de109abe7dcfa 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.unit.spec.tsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.unit.spec.tsx @@ -6,8 +6,13 @@ import { ChartSettingInputNumeric } from "./ChartSettingInputNumeric"; function setup({ value, + options, }: { value?: number; + options?: { + isInteger?: boolean; + isNonNegative?: boolean; + }; } = {}) { const onChange = jest.fn(); @@ -16,6 +21,7 @@ function setup({ value={value} onChange={onChange} onChangeSettings={() => null} + options={options} />, ); @@ -65,10 +71,48 @@ describe("ChartSettingInputNumber", () => { const { input, onChange } = setup(); await type({ input, value: "1.5e3" }); - expect(input).toHaveDisplayValue("1.5e3"); + expect(input).toHaveDisplayValue("1500"); expect(onChange).toHaveBeenCalledWith(1.5e3); }); + it("rounds decimals to integers when `isInteger` is `true`", async () => { + const { input, onChange } = setup({ options: { isInteger: true } }); + + await type({ input, value: "4.9" }); + expect(input).toHaveDisplayValue("5"); + expect(onChange).toHaveBeenCalledWith(5); + + await type({ input, value: "79512.3e-3" }); + expect(input).toHaveDisplayValue("80"); + expect(onChange).toHaveBeenCalledWith(80); + }); + + it("makes negatives positive when `isNonNegative` is `true`", async () => { + const { input, onChange } = setup({ options: { isNonNegative: true } }); + + await type({ input, value: "-5.4" }); + expect(input).toHaveDisplayValue("5.4"); + expect(onChange).toHaveBeenCalledWith(5.4); + + await type({ input, value: "-5.4e3" }); + expect(input).toHaveDisplayValue("5400"); + expect(onChange).toHaveBeenCalledWith(5400); + }); + + it("rounds decimals and makes them postiive when `isInteger` and `isNonNegative` are `true`", async () => { + const { input, onChange } = setup({ + options: { isInteger: true, isNonNegative: true }, + }); + + await type({ input, value: "-254.953" }); + expect(input).toHaveDisplayValue("255"); + expect(onChange).toHaveBeenCalledWith(255); + + await type({ input, value: "-9.4123458e3" }); + expect(input).toHaveDisplayValue("9412"); + expect(onChange).toHaveBeenCalledWith(9412); + }); + it("does not allow non-numeric values", async () => { const { input, onChange } = setup(); diff --git a/frontend/src/metabase/visualizations/echarts/pie/format.ts b/frontend/src/metabase/visualizations/echarts/pie/format.ts index f125d3e50bada45ad9307d66eea1f10ef9d4d0b7..9163cc1eb6154be0de5cd5bee83070efb8c4fae5 100644 --- a/frontend/src/metabase/visualizations/echarts/pie/format.ts +++ b/frontend/src/metabase/visualizations/echarts/pie/format.ts @@ -39,19 +39,25 @@ export function getPieChartFormatters( ...metricColSettings, }); - const formatPercent = (value: unknown, location: "legend" | "chart") => - renderingContext.formatValue(value, { - column: metricColSettings.column, - number_separators: metricColSettings.number_separators as string, - number_style: "percent", - decimals: computeMaxDecimalsForValues( + const formatPercent = (value: unknown, location: "legend" | "chart") => { + let decimals = settings["pie.decimal_places"]; + if (decimals == null) { + decimals = computeMaxDecimalsForValues( chartModel.slices.map(s => s.data.normalizedPercentage), { style: "percent", maximumSignificantDigits: location === "legend" ? 3 : 2, }, - ), + ); + } + + return renderingContext.formatValue(value, { + column: metricColSettings.column, + number_separators: metricColSettings.number_separators as string, + number_style: "percent", + decimals, }); + }; return { formatDimension, formatMetric, formatPercent }; } diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart/chart-definition.ts b/frontend/src/metabase/visualizations/visualizations/PieChart/chart-definition.ts index fcc164a196fb2c5bf975dfe1b71b3739e442ca4a..e6a7c01dc79394dfbf7a1ebe23496e66235091a2 100644 --- a/frontend/src/metabase/visualizations/visualizations/PieChart/chart-definition.ts +++ b/frontend/src/metabase/visualizations/visualizations/PieChart/chart-definition.ts @@ -112,6 +112,19 @@ export const PIE_CHART_DEFINITION: VisualizationDefinition = { ], }, }, + "pie.decimal_places": { + section: t`Display`, + title: t`Number of decimal places`, + widget: "number", + props: { + placeholder: t`Auto`, + options: { isInteger: true, isNonNegative: true }, + }, + getHidden: (_, settings) => + settings["pie.percent_visibility"] == null || + settings["pie.percent_visibility"] === "off", + readDependencies: ["pie.percent_visibility"], + }, "pie.slice_threshold": { section: t`Display`, title: t`Minimum slice percentage`,