diff --git a/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Ordered_Rows.png b/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Ordered_Rows.png new file mode 100644 index 0000000000000000000000000000000000000000..3a849cc33ff90b7acc3280835744626d19a3a473 Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Ordered_Rows.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Unordered_Rows.png b/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Unordered_Rows.png new file mode 100644 index 0000000000000000000000000000000000000000..9409326afae870bba87f9d489373fa68f7dd4c45 Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_FunnelBarChart_Funnel_Bar_Unordered_Rows.png differ diff --git a/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx b/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx index c2857a3669fdc60ee9edf84c82f750d6b0b4fe44..92a03b45bc0139cb6761f31ae04e76033f52cd13 100644 --- a/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx @@ -37,3 +37,17 @@ Default.args = { dashcardSettings: {}, renderingContext, }; + +export const FunnelBarOrderedRows = Template.bind({}); +FunnelBarOrderedRows.args = { + rawSeries: data.funnelBarOrderedRows as any, + dashcardSettings: {}, + renderingContext, +}; + +export const FunnelBarUnorderedRows = Template.bind({}); +FunnelBarUnorderedRows.args = { + rawSeries: data.funnelBarUnorderedRows as any, + dashcardSettings: {}, + renderingContext, +}; diff --git a/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-ordered-rows.json b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-ordered-rows.json new file mode 100644 index 0000000000000000000000000000000000000000..e4a52e753cfb7362f5803c1c8af532117cce7c32 --- /dev/null +++ b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-ordered-rows.json @@ -0,0 +1,296 @@ +[ + { + "card": { + "original_card_id": 438, + "can_delete": false, + "public_uuid": null, + "parameter_usage_count": 0, + "created_at": "2024-06-29T01:50:01.337123Z", + "parameters": [], + "metabase_version": "v0.2.0-SNAPSHOT (aac751c)", + "collection": { + "metabase.models.collection.root/is-root?": true, + "authority_level": null, + "name": "Our analytics", + "is_personal": false, + "id": "root", + "can_write": true + }, + "visualization_settings": { + "funnel.rows": [ + { + "key": "Gizmo", + "name": "Gizmo", + "enabled": false + }, + { + "key": "Gadget", + "name": "Gadget", + "enabled": true + }, + { + "key": "Doohickey", + "name": "Doohickey", + "enabled": true + }, + { + "key": "Widget", + "name": "Widget", + "enabled": true + } + ], + "funnel.order_dimension": "CATEGORY", + "funnel.type": "bar" + }, + "collection_preview": true, + "entity_id": "Hdp2_6fTuVCZNPrfu1iMa", + "archived_directly": false, + "display": "funnel", + "parameter_mappings": [], + "id": 438, + "dataset_query": { + "database": 1, + "type": "query", + "query": { + "source-table": 5, + "aggregation": [["count"]], + "breakout": [ + [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ] + ] + } + }, + "cache_ttl": null, + "embedding_params": null, + "made_public_by_id": null, + "updated_at": "2024-06-29T01:50:01.337123Z", + "moderation_reviews": [], + "can_restore": false, + "creator_id": 1, + "average_query_time": null, + "type": "question", + "last_used_at": null, + "dashboard_count": 0, + "last_query_start": null, + "name": "funnel bar ordered", + "query_type": "query", + "collection_id": null, + "enable_embedding": false, + "database_id": 1, + "trashed_from_collection_id": null, + "can_write": true, + "initially_published_at": null, + "result_metadata": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "id": 58, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "name": "count", + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 3976, + "q1": 4380, + "q3": 5000, + "max": 5061, + "sd": 489.3103990992493, + "avg": 4690 + } + } + } + } + ], + "can_run_adhoc_query": true, + "table_id": 5, + "collection_position": null, + "view_count": 0, + "archived": false, + "description": null, + "cache_invalidated_at": null, + "displayIsLocked": true + }, + "data": { + "rows": [ + ["Doohickey", 3976], + ["Gadget", 4939], + ["Gizmo", 4784], + ["Widget", 5061] + ], + "cols": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "table_id": 8, + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "source": "breakout", + "fk_target_field_id": null, + "fk_field_id": 40, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "nfc_path": null, + "parent_id": null, + "id": 58, + "position": 3, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text", + "source_alias": "PRODUCTS__via__PRODUCT_ID" + }, + { + "base_type": "type/BigInteger", + "name": "count", + "display_name": "Count", + "semantic_type": "type/Quantity", + "source": "aggregation", + "field_ref": ["aggregation", 0], + "aggregation_index": 0, + "effective_type": "type/BigInteger" + } + ], + "native_form": { + "query": "SELECT \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" AS \"PRODUCTS__via__PRODUCT_ID__CATEGORY\", COUNT(*) AS \"count\" FROM \"PUBLIC\".\"ORDERS\" LEFT JOIN \"PUBLIC\".\"PRODUCTS\" AS \"PRODUCTS__via__PRODUCT_ID\" ON \"PUBLIC\".\"ORDERS\".\"PRODUCT_ID\" = \"PRODUCTS__via__PRODUCT_ID\".\"ID\" GROUP BY \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" ORDER BY \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" ASC", + "params": null + }, + "format-rows?": true, + "results_timezone": "America/Toronto", + "requested_timezone": "Etc/GMT", + "results_metadata": { + "columns": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "id": 58, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "name": "count", + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 3976, + "q1": 4380, + "q3": 5000, + "max": 5061, + "sd": 489.3103990992493, + "avg": 4690 + } + } + } + } + ] + }, + "insights": null + } + } +] diff --git a/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-unordered-rows.json b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-unordered-rows.json new file mode 100644 index 0000000000000000000000000000000000000000..84ee78bfe0d6ca97b2a17606b2141860b061ca5f --- /dev/null +++ b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/funnel-bar-unordered-rows.json @@ -0,0 +1,273 @@ +[ + { + "card": { + "original_card_id": 438, + "can_delete": false, + "public_uuid": null, + "parameter_usage_count": 0, + "created_at": "2024-06-29T01:50:01.337123Z", + "parameters": [], + "metabase_version": "v0.2.0-SNAPSHOT (aac751c)", + "collection": { + "metabase.models.collection.root/is-root?": true, + "authority_level": null, + "name": "Our analytics", + "is_personal": false, + "id": "root", + "can_write": true + }, + "visualization_settings": { + "funnel.type": "bar" + }, + "collection_preview": true, + "entity_id": "Hdp2_6fTuVCZNPrfu1iMa", + "archived_directly": false, + "display": "funnel", + "parameter_mappings": [], + "id": 438, + "dataset_query": { + "database": 1, + "type": "query", + "query": { + "source-table": 5, + "aggregation": [["count"]], + "breakout": [ + [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ] + ] + } + }, + "cache_ttl": null, + "embedding_params": null, + "made_public_by_id": null, + "updated_at": "2024-06-29T01:50:01.337123Z", + "moderation_reviews": [], + "can_restore": false, + "creator_id": 1, + "average_query_time": null, + "type": "question", + "last_used_at": null, + "dashboard_count": 0, + "last_query_start": null, + "name": "funnel bar ordered", + "query_type": "query", + "collection_id": null, + "enable_embedding": false, + "database_id": 1, + "trashed_from_collection_id": null, + "can_write": true, + "initially_published_at": null, + "result_metadata": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "id": 58, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "name": "count", + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 3976, + "q1": 4380, + "q3": 5000, + "max": 5061, + "sd": 489.3103990992493, + "avg": 4690 + } + } + } + } + ], + "can_run_adhoc_query": true, + "table_id": 5, + "collection_position": null, + "view_count": 0, + "archived": false, + "description": null, + "cache_invalidated_at": null, + "displayIsLocked": true + }, + "data": { + "rows": [ + ["Doohickey", 3976], + ["Gadget", 4939], + ["Gizmo", 4784], + ["Widget", 5061] + ], + "cols": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "table_id": 8, + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "source": "breakout", + "fk_target_field_id": null, + "fk_field_id": 40, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "nfc_path": null, + "parent_id": null, + "id": 58, + "position": 3, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text", + "source_alias": "PRODUCTS__via__PRODUCT_ID" + }, + { + "base_type": "type/BigInteger", + "name": "count", + "display_name": "Count", + "semantic_type": "type/Quantity", + "source": "aggregation", + "field_ref": ["aggregation", 0], + "aggregation_index": 0, + "effective_type": "type/BigInteger" + } + ], + "native_form": { + "query": "SELECT \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" AS \"PRODUCTS__via__PRODUCT_ID__CATEGORY\", COUNT(*) AS \"count\" FROM \"PUBLIC\".\"ORDERS\" LEFT JOIN \"PUBLIC\".\"PRODUCTS\" AS \"PRODUCTS__via__PRODUCT_ID\" ON \"PUBLIC\".\"ORDERS\".\"PRODUCT_ID\" = \"PRODUCTS__via__PRODUCT_ID\".\"ID\" GROUP BY \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" ORDER BY \"PRODUCTS__via__PRODUCT_ID\".\"CATEGORY\" ASC", + "params": null + }, + "format-rows?": true, + "results_timezone": "America/Toronto", + "requested_timezone": "Etc/GMT", + "results_metadata": { + "columns": [ + { + "description": "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget", + "semantic_type": "type/Category", + "coercion_strategy": null, + "name": "CATEGORY", + "settings": null, + "fk_target_field_id": null, + "field_ref": [ + "field", + 58, + { + "base-type": "type/Text", + "source-field": 40 + } + ], + "effective_type": "type/Text", + "id": 58, + "visibility_type": "normal", + "display_name": "Product → Category", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Text": { + "percent-json": 0, + "percent-url": 0, + "percent-email": 0, + "percent-state": 0, + "average-length": 6.375 + } + } + }, + "base_type": "type/Text" + }, + { + "display_name": "Count", + "semantic_type": "type/Quantity", + "field_ref": ["aggregation", 0], + "name": "count", + "base_type": "type/BigInteger", + "effective_type": "type/BigInteger", + "fingerprint": { + "global": { + "distinct-count": 4, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 3976, + "q1": 4380, + "q3": 5000, + "max": 5061, + "sd": 489.3103990992493, + "avg": 4690 + } + } + } + } + ] + }, + "insights": null + } + } +] diff --git a/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/index.ts b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/index.ts index 2c2e07bc32f62c921d38ee09b2a45e201d28fbc2..00fb41039f3fac4af15096c964f5a0025f30c63f 100644 --- a/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/index.ts +++ b/frontend/src/metabase/static-viz/components/FunnelBarChart/stories-data/index.ts @@ -1,5 +1,9 @@ import funnelBarCategorical from "./funnel-bar-categorical.json"; +import funnelBarOrderedRows from "./funnel-bar-ordered-rows.json"; +import funnelBarUnorderedRows from "./funnel-bar-unordered-rows.json"; export const data = { funnelBarCategorical, + funnelBarOrderedRows, + funnelBarUnorderedRows, }; diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel/funnel-bar-transform.ts b/frontend/src/metabase/visualizations/visualizations/Funnel/funnel-bar-transform.ts index a845c87b4859008a2da33a4100fe8d3acb2ffd3f..934e32ddbd64a8ce2dd60a1c7224e7c5050d276a 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel/funnel-bar-transform.ts +++ b/frontend/src/metabase/visualizations/visualizations/Funnel/funnel-bar-transform.ts @@ -1,4 +1,6 @@ +import { isNotNull } from "metabase/lib/types"; import type { TransformSeries } from "metabase/visualizations/components/TransformedVisualization"; +import type { RowValue } from "metabase-types/api"; export const funnelToBarTransform: TransformSeries = ( rawSeries, @@ -18,7 +20,21 @@ export const funnelToBarTransform: TransformSeries = ( col => col.name === settings["funnel.metric"], ); - return rows.map(row => { + const rowByDimensionValue = rows.reduce((acc, row) => { + acc.set(row[dimensionIndex], row); + return acc; + }, new Map<RowValue, RowValue[]>()); + const rowsOrder = settings["funnel.rows"]; + const orderedRows = + Array.isArray(rowsOrder) && rowsOrder.length > 0 + ? rowsOrder + .map(rowOrder => + rowOrder.enabled ? rowByDimensionValue.get(rowOrder.key) : null, + ) + .filter(isNotNull) + : rows; + + return orderedRows.map(row => { const name = renderingContext.formatValue(row[dimensionIndex], { column: cols[dimensionIndex], });