Skip to main content
Glama
PulkitXChadha

Databricks MCP Server

widget_specs.py66.5 kB
"""Widget specification functions for all Lakeview widget types. Simple, direct widget creation with proper Lakeview JSON structure. Supports all 16 widget types with correct encodings and specifications. """ import uuid from typing import Any, Dict, List # Widget version mapping according to schema requirements # Each widget type has a specific version that matches Lakeview's schema expectations WIDGET_VERSIONS = { # Chart widgets - mostly version 3 for advanced features 'bar': 3, 'line': 3, 'area': 3, 'scatter': 3, 'heatmap': 3, 'histogram': 3, 'pie': 3, 'box': 3, 'funnel': 3, 'combo': 3, # Specialized widgets - version 1 for basic functionality 'sankey': 1, 'pivot': 1, 'table': 1, # Filter widgets - version 2 for parameter support 'filter-single-select': 2, 'filter-multi-select': 2, 'filter-date-range-picker': 2, # Advanced widgets 'range-slider': 3, 'counter': 2, # Map widgets - version 3 for geographic features 'choropleth-map': 3, 'symbol-map': 3, } def generate_id() -> str: """Generate 8-character hex ID for Lakeview objects. Lakeview requires unique identifiers for widgets, datasets, and other objects. This function creates short, readable IDs by truncating UUID4 strings. """ return str(uuid.uuid4())[:8] # Simple SQL Expression Helper Functions (Phase 1 Enhancement) # These functions help generate common SQL expressions for widget field transformations def get_aggregation_expression(field: str, func: str) -> str: """Return aggregation SQL expression - direct and simple. Args: field: Column name to aggregate func: Aggregation function (SUM, AVG, COUNT, etc.) Returns: SQL expression string like 'SUM(`field_name`)' Examples: get_aggregation_expression("revenue", "sum") -> "SUM(`revenue`)" get_aggregation_expression("customer_id", "count") -> "COUNT(`customer_id`)" get_aggregation_expression("score", "avg") -> "AVG(`score`)" """ return f'{func.upper()}(`{field}`)' def get_date_trunc_expression(field: str, interval: str) -> str: """Return date truncation SQL expression - direct and simple. Args: field: Date/timestamp column name interval: Date interval (DAY, MONTH, YEAR, HOUR, etc.) Returns: SQL expression string like 'DATE_TRUNC("MONTH", `field_name`)' Examples: get_date_trunc_expression("date", "month") -> 'DATE_TRUNC("MONTH", `date`)' get_date_trunc_expression("timestamp", "day") -> 'DATE_TRUNC("DAY", `timestamp`)' get_date_trunc_expression("created_at", "year") -> 'DATE_TRUNC("YEAR", `created_at`)' """ return f'DATE_TRUNC("{interval.upper()}", `{field}`)' def get_bin_expression(field: str, width: int) -> str: """Return binning SQL expression - direct and simple. Args: field: Numeric column name to bin width: Bin width for grouping values Returns: SQL expression string like 'BIN_FLOOR(`field_name`, 10)' Examples: get_bin_expression("score", 10) -> "BIN_FLOOR(`score`, 10)" get_bin_expression("age", 5) -> "BIN_FLOOR(`age`, 5)" get_bin_expression("value", 100) -> "BIN_FLOOR(`value`, 100)" """ return f'BIN_FLOOR(`{field}`, {width})' def get_count_star_expression() -> str: """Return count(*) expression - direct and simple. Returns: SQL expression string 'COUNT(`*`)' Example: get_count_star_expression() -> "COUNT(`*`)" """ return 'COUNT(`*`)' def validate_expression_basic(expression: str) -> Dict[str, Any]: """Simple validation for common SQL patterns - no complex schemas. Args: expression: SQL expression string to validate Returns: Dict with validation result: {"valid": bool, "error": str|None} Examples: validate_expression_basic("SUM(`revenue`)") -> {"valid": True, "error": None} validate_expression_basic("DROP TABLE users") -> { "valid": False, "error": "Potentially dangerous pattern: DROP" } validate_expression_basic("revenue") -> { "valid": False, "error": "Expression should reference fields with backticks" } """ try: # Basic checks for SQL injection patterns - prevent dangerous operations dangerous_patterns = ['DROP', 'DELETE', 'UPDATE', 'INSERT', '--', ';'] expression_upper = expression.upper() # Scan for potentially harmful SQL keywords for pattern in dangerous_patterns: if pattern in expression_upper: return {'valid': False, 'error': f'Potentially dangerous pattern: {pattern}'} # Check for basic SQL structure - fields should be backtick-quoted in Databricks if '`' not in expression and expression != 'COUNT(`*)': return {'valid': False, 'error': 'Expression should reference fields with backticks'} return {'valid': True, 'error': None} except Exception as e: return {'valid': False, 'error': str(e)} def find_dataset_id(dataset_name: str, datasets: List[Dict[str, Any]]) -> str: """Find dataset ID by display name. Searches through the datasets list to find a matching displayName and returns the corresponding dataset ID (name field). Falls back to first available dataset or generates a new ID if no match is found. Args: dataset_name: Human-readable dataset name to search for datasets: List of dataset dictionaries with 'displayName' and 'name' fields Returns: Dataset ID string for use in widget queries """ # Search for exact displayName match for ds in datasets: if ds['displayName'] == dataset_name: return ds['name'] # Fallback: return the first dataset or generate new ID if none available return datasets[0]['name'] if datasets else generate_id() def create_standard_axis_encoding( field_name: str, scale_type: str, config: Dict, encoding_type: str = None ) -> Dict: """Create standardized axis encoding with required scale structure. This function builds the encoding structure that Lakeview expects for chart axes. It handles scale types, display names, axis titles, and sorting configuration. Args: field_name: The field name for the encoding (must match dataset column) scale_type: Scale type (categorical, quantitative, temporal) config: Widget configuration containing additional settings encoding_type: Optional encoding type for config key lookups (x, y, color, etc.) Returns: Standardized encoding structure with scale, axis, and display settings """ # Base encoding structure with field name and scale type encoding = {'fieldName': field_name, 'scale': {'type': scale_type}} # Add display name for user-friendly axis labels if encoding_type: display_name_key = f'{encoding_type}_display_name' if display_name_key in config: encoding['displayName'] = config[display_name_key] else: encoding['displayName'] = field_name # Fallback to field name else: encoding['displayName'] = config.get(f'{field_name}_display_name', field_name) # Add axis configuration if custom title is specified if encoding_type: axis_title_key = f'{encoding_type}_axis_title' if axis_title_key in config: encoding['axis'] = {'title': config[axis_title_key]} # Add sort configuration for categorical scales (for ordering categories) if scale_type == 'categorical' and encoding_type: sort_key = f'{encoding_type}_sort' if sort_key in config: encoding['scale']['sort'] = {'by': config[sort_key]} return encoding def create_color_scale(scale_type: str, config: Dict) -> Dict[str, Any]: """Create color scale with ramp and mapping support. Color scales control how data values map to colors in visualizations. Supports both quantitative (continuous) and categorical (discrete) color schemes. Args: scale_type: Scale type (categorical, quantitative) config: Widget configuration with color settings Returns: Color scale configuration with ramps or mappings """ scale = {'type': scale_type} # Add color ramp for quantitative scales (continuous color gradients) if scale_type == 'quantitative' and 'color_scheme' in config: scale['colorRamp'] = { 'mode': 'scheme', 'scheme': config['color_scheme'], # Built-in schemes: redblue, viridis, plasma, etc. } # Add custom mappings for categorical scales (discrete color assignments) if scale_type == 'categorical' and 'color_mappings' in config: scale['mappings'] = config['color_mappings'] # List of {value: str, color: str} mappings return scale def create_advanced_encoding( field_name: str, config: Dict[str, Any], encoding_type: str ) -> Dict[str, Any]: """Create advanced encoding with scale, axis, and display configuration. This is the main encoding builder that handles all widget encoding types with intelligent defaults and advanced configuration options like custom scales, axis titles, legends, and sorting. Args: field_name: The field name for the encoding (must match dataset column) config: Widget configuration containing scale/axis settings encoding_type: Type of encoding (x, y, color, size, etc.) Returns: Advanced encoding structure with scale, axis, and display settings """ encoding = {'fieldName': field_name} # Determine scale type from config or use intelligent defaults scale_key = f'{encoding_type}_scale_type' scale_type = config.get(scale_key) # Set default scale types based on encoding type if not explicitly provided if scale_type is None: if encoding_type == 'x': scale_type = 'categorical' # X-axis typically categorical for bar charts elif encoding_type == 'y': scale_type = 'quantitative' # Y-axis typically quantitative for measurements elif encoding_type == 'color': scale_type = 'categorical' # Color typically categorical for grouping else: scale_type = 'quantitative' # Default for other encodings like size # Create scale configuration - color scales need special handling if encoding_type == 'color': encoding['scale'] = create_color_scale(scale_type, config) else: scale = {'type': scale_type} # Add sort configuration for categorical scales (controls category ordering) if scale_type == 'categorical': sort_key = f'{encoding_type}_sort' if sort_key in config: scale['sort'] = {'by': config[sort_key]} encoding['scale'] = scale # Add axis configuration for custom axis titles axis_title_key = f'{encoding_type}_axis_title' if axis_title_key in config: encoding['axis'] = {'title': config[axis_title_key]} # Add display name for user-friendly labels display_name_key = f'{encoding_type}_display_name' if display_name_key in config: encoding['displayName'] = config[display_name_key] else: encoding['displayName'] = field_name # Fallback to field name # Add legend configuration for color encoding if encoding_type == 'color' and 'legend_title' in config: encoding['legend'] = {'title': config['legend_title']} return encoding def create_frame_config(config: Dict[str, Any]) -> Dict[str, Any]: """Create frame configuration for widget title and display. The frame controls the widget's outer appearance including title display. This is separate from the chart content and appears at the top of each widget. Args: config: Widget configuration dictionary Returns: Frame configuration dict with title and display settings """ frame = {} # Add title configuration if specified if 'title' in config: frame['title'] = config['title'] frame['showTitle'] = config.get('show_title', True) # Default to showing title return frame def create_widget_queries( widget_config: Dict[str, Any], datasets: List[Dict] ) -> List[Dict[str, Any]]: """Create widget queries with field expressions and aggregations. This function generates the query structure that tells Lakeview how to fetch and transform data for each widget. It automatically creates field expressions for all referenced fields and supports custom SQL expressions. Based on analysis of actual Lakeview dashboard examples, widgets typically need a 'fields' array in the query to specify which fields to use and how to aggregate them. This function now generates fields arrays by default for all widgets with field references. Args: widget_config: Complete widget configuration including dataset and field references datasets: List of available datasets for ID lookup Returns: List containing a single query dict with dataset reference and field expressions """ config = widget_config.get('config', {}) dataset_id = find_dataset_id(widget_config['dataset'], datasets) # Base query structure with dataset reference and aggregation setting query = {'datasetName': dataset_id, 'disaggregated': config.get('disaggregated', False)} # Build fields array for all field references in the widget # This matches the structure seen in real Lakeview dashboard examples fields = [] # Standard field keys that widgets commonly use # Each corresponds to a different encoding type (x-axis, y-axis, color, etc.) for field_key in [ 'x_field', # X-axis field for charts 'y_field', # Y-axis field for charts 'color_field', # Color grouping field 'size_field', # Size encoding field (for bubble charts, etc.) 'value_field', # Value field for counters, pie charts 'category_field', # Category field for pie charts, filters 'source_field', # Source field for Sankey diagrams 'target_field', # Target field for Sankey diagrams 'stage_field', # Stage field for funnel charts 'location_field', # Location field for maps 'latitude_field', # Latitude field for symbol maps 'longitude_field', # Longitude field for symbol maps ]: if field_key in config: field_name = config[field_key] # Check if there's a custom SQL expression for this field expression_key = field_key.replace('_field', '_expression') if expression_key in config: # Use custom expression (e.g., "SUM(`revenue`)", "DATE_TRUNC('MONTH', `date`)") fields.append({'name': field_name, 'expression': config[expression_key]}) else: # Default expression: direct field reference with backticks (Databricks standard) # This matches the format seen in example dashboards: "`field_name`" fields.append({'name': field_name, 'expression': f'`{field_name}`'}) # Special handling for table widgets with column arrays if 'columns' in config and isinstance(config['columns'], list): for col in config['columns']: # Avoid duplicates if column is already added via other field keys if not any(f['name'] == col for f in fields): fields.append({'name': col, 'expression': f'`{col}`'}) # Add fields array to query if we have any fields # This ensures the query format matches actual Lakeview dashboard examples if fields: query['fields'] = fields # Return as single-item list with named query (Lakeview expects this structure) return [{'name': 'main_query', 'query': query}] def create_widget_spec( widget_config: Dict[str, Any], datasets: List[Dict], dashboard_id: str = None ) -> Dict[str, Any]: """Create widget spec for any widget type with advanced Lakeview features. This is the main widget factory function that routes widget creation to the appropriate specialized function based on widget type. It supports all 16+ Lakeview widget types with backward compatibility for legacy names. Args: widget_config: Widget configuration with structure: { "type": str, # Widget type (bar, line, table, etc.) "dataset": str, # Dataset name to use "config": { "x_field": str, # Field mappings "y_field": str, "x_scale_type": "categorical|quantitative|temporal", "y_scale_type": "categorical|quantitative|temporal", "color_mappings": [{"value": str, "color": str}], "title": str, # Widget title "show_title": bool, # Show/hide title "version": int, # Widget version override "custom_expressions": bool # Enable custom SQL expressions } } datasets: List of available datasets for field validation dashboard_id: Optional dashboard ID for filter widget parameter generation Returns: Complete widget specification for Lakeview dashboard with advanced features """ widget_type = widget_config.get('type', 'table') # Default to table if type not specified # Chart widgets - visualization types for data analysis if widget_type == 'bar': return create_advanced_bar_widget(widget_config, datasets) elif widget_type == 'line': return create_advanced_line_widget(widget_config, datasets) elif widget_type == 'area': return create_advanced_area_widget(widget_config, datasets) elif widget_type == 'scatter': return create_advanced_scatter_widget(widget_config, datasets) elif widget_type == 'pie': return create_advanced_pie_widget(widget_config, datasets) elif widget_type == 'histogram': return create_advanced_histogram_widget(widget_config, datasets) elif widget_type == 'heatmap': return create_advanced_heatmap_widget(widget_config, datasets) elif widget_type == 'box': return create_box_widget(widget_config, datasets) elif widget_type == 'sankey': return create_sankey_widget(widget_config, datasets) elif widget_type == 'choropleth-map': return create_choropleth_widget(widget_config, datasets) elif widget_type == 'symbol-map': return create_symbol_map_widget(widget_config, datasets) elif widget_type == 'funnel': return create_funnel_widget(widget_config, datasets) elif widget_type == 'combo': return create_combo_widget(widget_config, datasets) elif widget_type == 'range-slider': return create_range_slider_widget(widget_config, datasets) # Display widgets - for showing data in tabular or summary formats elif widget_type == 'counter': return create_advanced_counter_widget(widget_config, datasets) elif widget_type == 'table': return create_advanced_table_widget(widget_config, datasets) elif widget_type == 'pivot': return create_advanced_pivot_widget(widget_config, datasets) elif widget_type == 'text': return create_advanced_text_widget(widget_config, datasets) # Filter widgets - Standardized versions with parameter support for dashboard interactivity elif widget_type == 'filter-single-select': return create_filter_single_select_widget(widget_config, datasets, dashboard_id) elif widget_type == 'filter-multi-select': return create_filter_multi_select_widget(widget_config, datasets, dashboard_id) elif widget_type == 'filter-date-range-picker': return create_filter_date_range_widget(widget_config, datasets, dashboard_id) elif widget_type == 'filter-date-range': # Legacy compatibility return create_filter_date_range_widget(widget_config, datasets, dashboard_id) # Legacy filter widget names (for backward compatibility with older configurations) elif widget_type == 'dropdown': return create_filter_single_select_widget(widget_config, datasets, dashboard_id) elif widget_type == 'multi_select': return create_filter_multi_select_widget(widget_config, datasets, dashboard_id) elif widget_type == 'date_range': return create_filter_date_range_widget(widget_config, datasets, dashboard_id) elif widget_type == 'slider': return create_slider_widget(widget_config, datasets) elif widget_type == 'text_search': return create_text_search_widget(widget_config, datasets) else: # Default fallback: create table widget for unknown types return create_advanced_table_widget(widget_config, datasets) # Advanced Chart Widgets # These functions create sophisticated chart widgets with full Lakeview feature support def create_advanced_bar_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced bar chart widget spec with scales, axes, and legends. Bar charts are ideal for comparing categorical data. This function supports grouped bars (via color encoding), custom scales, axis titles, and sorting. Args: config: Widget configuration containing field mappings and display options datasets: Available datasets for query generation Returns: Complete bar widget specification with encodings and queries """ widget_config = config.get('config', {}) encodings = {} # X-axis encoding with advanced features (typically categorical for bar charts) if 'x_field' in widget_config: encodings['x'] = create_advanced_encoding(widget_config['x_field'], widget_config, 'x') # Y-axis encoding with advanced features (typically quantitative for measurements) if 'y_field' in widget_config: encodings['y'] = create_advanced_encoding(widget_config['y_field'], widget_config, 'y') # Color encoding with custom mappings (for grouped/stacked bars) if 'color_field' in widget_config: encodings['color'] = create_advanced_encoding( widget_config['color_field'], widget_config, 'color' ) # Build widget spec with version and encodings spec = {'version': WIDGET_VERSIONS['bar'], 'widgetType': 'bar', 'encodings': encodings} # Add frame configuration (title, etc.) frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} # Legacy function for backward compatibility def create_bar_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy bar widget creation - redirects to advanced version. Maintained for backward compatibility with older code that uses the legacy function name. """ return create_advanced_bar_widget(config, datasets) def create_advanced_line_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced line chart widget spec with scales, axes, and legends. Line charts are perfect for showing trends over time or continuous data. Supports multiple series via color encoding and temporal/quantitative scales. Args: config: Widget configuration containing field mappings and display options datasets: Available datasets for query generation Returns: Complete line widget specification with encodings and queries """ widget_config = config.get('config', {}) encodings = {} # X-axis encoding (typically temporal for time series or quantitative for continuous data) if 'x_field' in widget_config: encodings['x'] = create_advanced_encoding(widget_config['x_field'], widget_config, 'x') # Y-axis encoding (quantitative values to plot) if 'y_field' in widget_config: encodings['y'] = create_advanced_encoding(widget_config['y_field'], widget_config, 'y') # Color encoding for multiple series (creates separate lines for each category) if 'color_field' in widget_config: encodings['color'] = create_advanced_encoding( widget_config['color_field'], widget_config, 'color' ) # Build complete widget specification spec = {'version': WIDGET_VERSIONS['line'], 'widgetType': 'line', 'encodings': encodings} # Add frame configuration for title display frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_line_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy line widget creation - redirects to advanced version.""" return create_advanced_line_widget(config, datasets) def create_advanced_area_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced area chart widget spec.""" widget_config = config.get('config', {}) encodings = {} # X-axis encoding if 'x_field' in widget_config: encodings['x'] = create_advanced_encoding(widget_config['x_field'], widget_config, 'x') # Y-axis encoding if 'y_field' in widget_config: encodings['y'] = create_advanced_encoding(widget_config['y_field'], widget_config, 'y') # Color encoding for stacking if 'color_field' in widget_config: encodings['color'] = create_advanced_encoding( widget_config['color_field'], widget_config, 'color' ) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['area'], 'widgetType': 'area', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_area_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy area widget creation - redirects to advanced version.""" return create_advanced_area_widget(config, datasets) def create_advanced_scatter_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced scatter plot widget spec with full encoding support.""" widget_config = config.get('config', {}) encodings = {} # X-axis encoding if 'x_field' in widget_config: encodings['x'] = create_advanced_encoding(widget_config['x_field'], widget_config, 'x') # Y-axis encoding if 'y_field' in widget_config: encodings['y'] = create_advanced_encoding(widget_config['y_field'], widget_config, 'y') # Color encoding with custom mappings (important for scatter plots) if 'color_field' in widget_config: encodings['color'] = create_advanced_encoding( widget_config['color_field'], widget_config, 'color' ) # Size encoding for bubble charts if 'size_field' in widget_config: encodings['size'] = create_advanced_encoding(widget_config['size_field'], widget_config, 'size') spec = {'version': WIDGET_VERSIONS['scatter'], 'widgetType': 'scatter', 'encodings': encodings} frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_scatter_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy scatter widget creation - redirects to advanced version.""" return create_advanced_scatter_widget(config, datasets) def create_advanced_pie_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create pie chart with correct angle/color encodings.""" widget_config = config.get('config', {}) encodings = {} # Angle encoding (value field) if 'value_field' in widget_config: encodings['angle'] = { 'fieldName': widget_config['value_field'], 'scale': {'type': 'quantitative'}, 'displayName': widget_config.get('value_display_name', widget_config['value_field']), } # Color encoding (category field) if 'category_field' in widget_config: encodings['color'] = { 'fieldName': widget_config['category_field'], 'scale': {'type': 'categorical'}, 'displayName': widget_config.get('category_display_name', widget_config['category_field']), } return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['pie'], 'widgetType': 'pie', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_pie_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy pie chart widget creation - redirects to advanced version.""" return create_advanced_pie_widget(config, datasets) def create_advanced_histogram_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced histogram widget spec. Histograms show the distribution of numeric data by grouping values into bins. This is one of the more complex widgets due to the binning requirements. Histograms require both X and Y encodings according to the Lakeview schema: - X: quantitative or temporal scale with binning for numeric distribution - Y: quantitative scale with count aggregation (count(*) by default) Critical: The query fields must match the encoding fieldNames exactly. For histograms, this means the query must provide binned fields with proper SQL expressions. Args: config: Widget configuration with x_field (numeric field to bin) and optional bin_width datasets: Available datasets for query generation Returns: Complete histogram widget specification with binned field expressions """ widget_config = config.get('config', {}) encodings = {} # X-field encoding with binning (required for histogram distribution) if 'x_field' in widget_config: x_field = widget_config['x_field'] bin_width = widget_config.get('bin_width', 10) # Default bin width for grouping # Create binned field expression for histograms - this creates the bins binned_field = f'bin({x_field}, binWidth={bin_width})' encodings['x'] = { 'fieldName': binned_field, # Must match the query field name exactly 'scale': {'type': 'quantitative'}, 'displayName': widget_config.get('x_display_name', x_field), } # Y-field encoding with count aggregation (required for histograms) # This counts how many records fall into each bin y_field = widget_config.get('y_field', 'count(*)') encodings['y'] = { 'fieldName': y_field, 'scale': {'type': 'quantitative'}, 'displayName': widget_config.get('y_display_name', 'Count of Records'), } # Prepare config for query generation with proper binned fields # This is critical: the query must provide the exact fields that encodings reference updated_config = config.copy() histogram_config = widget_config.copy() # Critical fix: Ensure the query fields match the encoding fieldNames exactly if 'x_field' in widget_config: x_field = widget_config['x_field'] bin_width = widget_config.get('bin_width', 10) # The binned field name must match what's in encodings binned_field = f'bin({x_field}, binWidth={bin_width})' # Set up both the field name and expression for the binned field histogram_config['x_field'] = binned_field # This matches the encoding fieldName histogram_config['x_expression'] = get_bin_expression( x_field, bin_width ) # This is the actual SQL expression: BIN_FLOOR(`field`, width) # Set up count field with proper expression histogram_config['y_field'] = y_field histogram_config['y_expression'] = ( get_count_star_expression() if y_field == 'count(*)' else y_field ) updated_config['config'] = histogram_config return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['histogram'], 'widgetType': 'histogram', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(updated_config, datasets), } def create_histogram_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy histogram widget creation - redirects to advanced version.""" return create_advanced_histogram_widget(config, datasets) def create_advanced_heatmap_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create heatmap widget with color ramp support.""" widget_config = config.get('config', {}) encodings = {} # X and Y axis encodings if 'x_field' in widget_config: encodings['x'] = { 'fieldName': widget_config['x_field'], 'scale': {'type': 'categorical'}, 'displayName': widget_config.get('x_display_name', widget_config['x_field']), } # Add axis title hiding option if widget_config.get('hide_x_title'): encodings['x']['axis'] = {'hideTitle': True} if 'y_field' in widget_config: encodings['y'] = { 'fieldName': widget_config['y_field'], 'scale': {'type': 'categorical'}, 'displayName': widget_config.get('y_display_name', widget_config['y_field']), } if widget_config.get('hide_y_title'): encodings['y']['axis'] = {'hideTitle': True} # Color encoding with color ramp - support both color_field and value_field color_field = widget_config.get('color_field') or widget_config.get('value_field') if color_field: color_scale = create_color_scale('quantitative', widget_config) encodings['color'] = { 'fieldName': color_field, 'scale': color_scale, 'displayName': widget_config.get('color_display_name') or widget_config.get('value_display_name', color_field), } return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['heatmap'], 'widgetType': 'heatmap', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_heatmap_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy heatmap widget creation - redirects to advanced version.""" return create_advanced_heatmap_widget(config, datasets) # Display Widgets # These widgets focus on presenting data in non-chart formats (tables, counters, text) def create_advanced_counter_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced counter widget spec with proper encoding structure. Counter widgets display a single numeric value prominently, often used for KPIs, totals, or summary statistics. They're perfect for dashboard headers or key metrics. Counter widgets have a simple value encoding without scale properties, according to the Lakeview schema and dashboard examples. Args: config: Widget configuration with value_field and optional display settings datasets: Available datasets for query generation Returns: Complete counter widget specification with value encoding """ widget_config = config.get('config', {}) encodings = {} # Value encoding with display name - NO scale property for counter widgets # Counter widgets are unique in that they don't use scale configurations if 'value_field' in widget_config: encodings['value'] = { 'fieldName': widget_config['value_field'], 'displayName': widget_config.get('value_display_name', widget_config['value_field']), } # Build widget specification spec = {'version': WIDGET_VERSIONS['counter'], 'widgetType': 'counter', 'encodings': encodings} # Add frame configuration for title display frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_counter_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Legacy counter widget creation - redirects to advanced version.""" return create_advanced_counter_widget(config, datasets) def create_table_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create table widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'columns' in widget_config: encodings['columns'] = [{'fieldName': col} for col in widget_config['columns']] return { 'name': generate_id(), 'spec': {'widgetType': 'table', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_pivot_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create pivot table widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'rows' in widget_config: encodings['rows'] = [{'fieldName': row} for row in widget_config['rows']] if 'columns' in widget_config: encodings['columns'] = [{'fieldName': col} for col in widget_config['columns']] if 'values' in widget_config: encodings['values'] = [{'fieldName': val} for val in widget_config['values']] return { 'name': generate_id(), 'spec': {'widgetType': 'pivot', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_text_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create text widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'text' in widget_config: encodings['text'] = widget_config['text'] if 'markdown' in widget_config: encodings['markdown'] = widget_config['markdown'] return { 'name': generate_id(), 'spec': {'widgetType': 'text', 'encodings': encodings}, 'queries': [] if not config.get('dataset') else [ { 'name': generate_id(), 'query': {'datasetName': find_dataset_id(config['dataset'], datasets)}, } ], } # Filter Widgets def create_dropdown_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create dropdown filter widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'field' in widget_config: encodings['field'] = {'fieldName': widget_config['field']} if 'label' in widget_config: encodings['label'] = widget_config['label'] return { 'name': generate_id(), 'spec': {'widgetType': 'dropdown', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_multi_select_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create multi-select filter widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'field' in widget_config: encodings['field'] = {'fieldName': widget_config['field']} if 'label' in widget_config: encodings['label'] = widget_config['label'] return { 'name': generate_id(), 'spec': {'widgetType': 'multiSelect', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_date_range_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create date range filter widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'field' in widget_config: encodings['field'] = {'fieldName': widget_config['field']} if 'label' in widget_config: encodings['label'] = widget_config['label'] if 'start_date' in widget_config: encodings['startDate'] = widget_config['start_date'] if 'end_date' in widget_config: encodings['endDate'] = widget_config['end_date'] return { 'name': generate_id(), 'spec': {'widgetType': 'dateRange', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_slider_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create slider filter widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'field' in widget_config: encodings['field'] = {'fieldName': widget_config['field']} if 'label' in widget_config: encodings['label'] = widget_config['label'] if 'min_value' in widget_config: encodings['minValue'] = widget_config['min_value'] if 'max_value' in widget_config: encodings['maxValue'] = widget_config['max_value'] return { 'name': generate_id(), 'spec': {'widgetType': 'slider', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } def create_text_search_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create text search filter widget spec.""" widget_config = config.get('config', {}) encodings = {} if 'field' in widget_config: encodings['field'] = {'fieldName': widget_config['field']} if 'label' in widget_config: encodings['label'] = widget_config['label'] if 'placeholder' in widget_config: encodings['placeholder'] = widget_config['placeholder'] return { 'name': generate_id(), 'spec': {'widgetType': 'textSearch', 'encodings': encodings}, 'queries': create_widget_queries(config, datasets), } # Advanced Widget Types def create_filter_single_select_widget( config: Dict, datasets: List[Dict], dashboard_id: str = None ) -> Dict[str, Any]: """Create standardized single-select filter widget. Single-select filters allow users to choose one value from a dropdown list, which then filters other widgets on the dashboard. These are essential for dashboard interactivity and require proper parameter configuration. Args: config: Widget configuration with field definitions and display options datasets: Available datasets for field validation dashboard_id: Dashboard ID for generating parameter query names Returns: Complete filter widget specification with field encodings """ widget_config = config.get('config', {}) fields = [] # Support both new and legacy field configurations for backward compatibility if 'fields' in widget_config: # New array-based field configuration (preferred approach) for field_config in widget_config['fields']: if isinstance(field_config, dict) and 'fieldName' in field_config: field = { 'fieldName': field_config['fieldName'], # Field to filter on 'displayName': field_config.get('displayName', field_config['fieldName']), # UI label } # QueryName links this filter to dashboard parameters if 'queryName' in field_config: field['queryName'] = field_config['queryName'] fields.append(field) elif 'field' in widget_config: # Legacy single field configuration (for backward compatibility) field = { 'fieldName': widget_config['field'], 'displayName': widget_config.get('display_name', widget_config['field']), } # Generate queryName from dataset if available (required for parameter binding) if 'query_name' in widget_config: field['queryName'] = widget_config['query_name'] elif datasets and dashboard_id: # Auto-generate queryName from first dataset for parameter system dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: field['queryName'] = ( f'dashboards/{dashboard_id}/datasets/{dataset_id}_{widget_config["field"]}' ) fields.append(field) # Fallback: if no fields configured, try to infer from query data if not fields and datasets and dashboard_id: # Get first available string/categorical field from dataset dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: # Create a default field - this should be configured properly by caller default_field = widget_config.get('default_field', 'category') field = { 'fieldName': default_field, 'displayName': default_field.replace('_', ' ').title(), # Convert snake_case to Title Case 'queryName': f'dashboards/{dashboard_id}/datasets/{dataset_id}_{default_field}', } fields.append(field) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['filter-single-select'], 'widgetType': 'filter-single-select', 'encodings': {'fields': fields}, # Fields array defines what can be filtered 'frame': create_frame_config(widget_config), }, # Note: Filter widgets typically don't have queries - they generate parameters instead } def create_filter_multi_select_widget( config: Dict, datasets: List[Dict], dashboard_id: str = None ) -> Dict[str, Any]: """Create standardized multi-select filter widget.""" widget_config = config.get('config', {}) fields = [] # Support both new and legacy field configurations if 'fields' in widget_config: # New array-based field configuration for field_config in widget_config['fields']: if isinstance(field_config, dict) and 'fieldName' in field_config: field = { 'fieldName': field_config['fieldName'], 'displayName': field_config.get('displayName', field_config['fieldName']), } if 'queryName' in field_config: field['queryName'] = field_config['queryName'] fields.append(field) elif 'field' in widget_config: # Legacy single field configuration field = { 'fieldName': widget_config['field'], 'displayName': widget_config.get('display_name', widget_config['field']), } # Generate queryName from dataset if available if 'query_name' in widget_config: field['queryName'] = widget_config['query_name'] elif datasets and dashboard_id: # Auto-generate queryName from first dataset dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: field['queryName'] = ( f'dashboards/{dashboard_id}/datasets/{dataset_id}_{widget_config["field"]}' ) fields.append(field) # If no fields configured, try to infer from query data if not fields and datasets and dashboard_id: # Get first available string/categorical field from dataset dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: # Create a default field - this should be configured properly by caller default_field = widget_config.get('default_field', 'category') field = { 'fieldName': default_field, 'displayName': default_field.replace('_', ' ').title(), 'queryName': f'dashboards/{dashboard_id}/datasets/{dataset_id}_{default_field}', } fields.append(field) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['filter-multi-select'], 'widgetType': 'filter-multi-select', 'encodings': {'fields': fields}, 'frame': create_frame_config(widget_config), }, } def create_filter_date_range_widget( config: Dict, datasets: List[Dict], dashboard_id: str = None ) -> Dict[str, Any]: """Create standardized date range filter widget.""" widget_config = config.get('config', {}) fields = [] # Support both new and legacy field configurations if 'fields' in widget_config: # New array-based field configuration for field_config in widget_config['fields']: if isinstance(field_config, dict) and 'fieldName' in field_config: field = { 'fieldName': field_config['fieldName'], 'displayName': field_config.get('displayName', field_config['fieldName']), } if 'queryName' in field_config: field['queryName'] = field_config['queryName'] fields.append(field) elif 'field' in widget_config: # Legacy single field configuration field = { 'fieldName': widget_config['field'], 'displayName': widget_config.get('display_name', widget_config['field']), } # Generate queryName from dataset if available if 'query_name' in widget_config: field['queryName'] = widget_config['query_name'] elif datasets and dashboard_id: # Auto-generate queryName from first dataset dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: field['queryName'] = ( f'dashboards/{dashboard_id}/datasets/{dataset_id}_{widget_config["field"]}' ) fields.append(field) # If no fields configured, try to infer from query data if not fields and datasets and dashboard_id: # Get first available date/temporal field from dataset dataset_id = find_dataset_id(widget_config.get('dataset', ''), datasets) if dataset_id: # Create a default field - this should be configured properly by caller default_field = widget_config.get('default_field', 'date') field = { 'fieldName': default_field, 'displayName': default_field.replace('_', ' ').title(), 'queryName': f'dashboards/{dashboard_id}/datasets/{dataset_id}_{default_field}', } fields.append(field) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['filter-date-range-picker'], 'widgetType': 'filter-date-range-picker', 'encodings': {'fields': fields}, 'frame': create_frame_config(widget_config), }, } def create_sankey_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create Sankey diagram widget. Sankey diagrams show flows between different stages or categories, with the width of flows proportional to the quantity. Perfect for visualizing data movement, user journeys, or resource allocation between different states. Args: config: Widget configuration with value_field, source_field, and target_field datasets: Available datasets for query generation Returns: Complete Sankey widget specification with value and stages encodings """ widget_config = config.get('config', {}) encodings = {} # Value field (required) - determines the width of the flow lines # This represents the quantity flowing from source to target if 'value_field' in widget_config: encodings['value'] = { 'fieldName': widget_config['value_field'], 'displayName': widget_config.get('value_display_name', 'Value'), } # Stages array (source and target fields) - defines the flow connections # Each stage represents a node in the Sankey diagram stages = [] if 'source_field' in widget_config: stages.append( { 'fieldName': widget_config['source_field'], 'displayName': widget_config.get('source_display_name', 'Source'), } ) if 'target_field' in widget_config: stages.append( { 'fieldName': widget_config['target_field'], 'displayName': widget_config.get('target_display_name', 'Target'), } ) # Add stages to encodings if we have any defined if stages: encodings['stages'] = stages # Build widget specification (note: Sankey uses version 1) spec = {'version': 1, 'widgetType': 'sankey', 'encodings': encodings} # Add frame configuration for title frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_box_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create box plot widget with correct Lakeview structure.""" widget_config = config.get('config', {}) encodings = {} # X-axis (categorical field) - REQUIRED for box plots x_field = widget_config.get('x_field') or widget_config.get('category_field') if x_field: encodings['x'] = {'fieldName': x_field, 'scale': {'type': 'categorical'}} else: # Box plots require an x-axis field return { 'name': generate_id(), 'spec': {'error': 'Box plots require an x_field or category_field'}, 'queries': [], } # Y-axis structure for box plots (requires statistical fields) y_encoding = {'scale': {'type': 'quantitative'}} # Box plot requires specific statistical fields if 'min_field' in widget_config: y_encoding['whiskerStart'] = {'fieldName': widget_config['min_field']} if 'q1_field' in widget_config: y_encoding['boxStart'] = {'fieldName': widget_config['q1_field']} if 'median_field' in widget_config: y_encoding['boxMid'] = {'fieldName': widget_config['median_field']} if 'q3_field' in widget_config: y_encoding['boxEnd'] = {'fieldName': widget_config['q3_field']} if 'max_field' in widget_config: y_encoding['whiskerEnd'] = {'fieldName': widget_config['max_field']} # If no statistical fields are provided, fall back to a simple value field if not any( key in y_encoding for key in ['whiskerStart', 'boxStart', 'boxMid', 'boxEnd', 'whiskerEnd'] ): if 'value_field' in widget_config: y_encoding['fieldName'] = widget_config['value_field'] else: # Y-axis requires at least one field return { 'name': generate_id(), 'spec': { 'error': 'Box plots require statistical fields (min_field, q1_field, etc.) or value_field' }, 'queries': [], } encodings['y'] = y_encoding spec = {'version': 3, 'widgetType': 'box', 'encodings': encodings} frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_choropleth_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create choropleth map widget with proper region encoding structure.""" widget_config = config.get('config', {}) encodings = {} # Color encoding for the quantitative value if 'color_field' in widget_config: encodings['color'] = create_advanced_encoding( widget_config['color_field'], widget_config, 'color' ) # Region encoding for geographical data if 'location_field' in widget_config: location_field = widget_config['location_field'] geo_type = widget_config.get('geo_type', 'state') # Default to state # Map geo_type to geographic role geo_role_mapping = { 'country': 'admin0-unit-code', 'state': 'admin1-unit-code', 'county': 'admin2-unit-code', 'zipcode': 'zipcode', } # Map geo_type to admin level admin_level_mapping = { 'country': 'admin0', 'state': 'admin1', 'county': 'admin2', 'zipcode': 'zipcode', } geo_role = geo_role_mapping.get(geo_type, 'admin1-unit-code') admin_level = admin_level_mapping.get(geo_type, 'admin1') encodings['region'] = { 'regionType': 'mapbox-v4-admin', admin_level: { 'fieldName': location_field, 'type': 'field', 'geographicRole': geo_role, 'displayName': widget_config.get('location_display_name', location_field.title()), }, } spec = { 'version': 1, # Using version 1 as per real examples, not schema version 3 'widgetType': 'choropleth-map', 'encodings': encodings, } frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} # Advanced implementations are now defined inline above def create_advanced_table_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create table widget with full column specifications. Table widgets display data in rows and columns with extensive formatting options. Supports column types, custom formatting, links, images, and search functionality. This is one of the most feature-rich widgets in Lakeview. Args: config: Widget configuration with columns array and table options datasets: Available datasets for query generation Returns: Complete table widget specification with column configurations """ widget_config = config.get('config', {}) columns = [] if 'columns' in widget_config: for i, col_config in enumerate(widget_config['columns']): if isinstance(col_config, str): # Simple column name - create basic column with defaults column = { 'fieldName': col_config, 'type': 'string', # Default data type 'displayAs': 'string', # Default display format 'visible': True, 'order': i, # Column order in table 'title': col_config, # Column header text } else: # Full column configuration with advanced options column = { 'fieldName': col_config['field'], 'type': col_config.get('type', 'string'), # Data type: string, number, boolean, date 'displayAs': col_config.get('display_as', 'string'), # Display format 'visible': col_config.get('visible', True), # Show/hide column 'order': col_config.get('order', i), # Column position 'title': col_config.get('title', col_config['field']), # Header text } # Add optional number/date formatting if 'number_format' in col_config: column['numberFormat'] = col_config['number_format'] # e.g., "#,##0.00" if 'date_format' in col_config: column['dateTimeFormat'] = col_config['date_format'] # e.g., "MM/dd/yyyy" # Add link configuration for clickable links if col_config.get('display_as') == 'link': column['linkUrlTemplate'] = col_config.get('link_url', '') # URL template column['linkTextTemplate'] = col_config.get('link_text', '') # Link text template column['linkOpenInNewTab'] = col_config.get('link_new_tab', True) # Open in new tab # Add image configuration for displaying images if col_config.get('display_as') == 'image': column['imageUrlTemplate'] = col_config.get('image_url', '') # Image URL template column['imageWidth'] = col_config.get('image_width', '100px') # Image width column['imageHeight'] = col_config.get('image_height', '100px') # Image height # Add boolean display values (how true/false are shown) if col_config.get('type') == 'boolean': column['booleanValues'] = col_config.get('boolean_values', ['false', 'true']) # Add content alignment (left, center, right) if 'align' in col_config: column['alignContent'] = col_config['align'] # Add search capability for this column column['allowSearch'] = col_config.get('allow_search', True) # Add HTML rendering support (for rich content) column['allowHTML'] = col_config.get('allow_html', False) columns.append(column) # Build table specification with column encodings spec = { 'version': WIDGET_VERSIONS['table'], 'widgetType': 'table', 'encodings': {'columns': columns}, } # Add table-specific display options if 'items_per_page' in widget_config: spec['itemsPerPage'] = widget_config['items_per_page'] # Rows per page if 'pagination_size' in widget_config: spec['paginationSize'] = widget_config['pagination_size'] # Pagination control size if 'condensed' in widget_config: spec['condensed'] = widget_config['condensed'] # Compact row spacing if 'with_row_number' in widget_config: spec['withRowNumber'] = widget_config['with_row_number'] # Show row numbers if 'allow_html_default' in widget_config: spec['allowHTMLByDefault'] = widget_config['allow_html_default'] # Default HTML rendering # Add frame configuration for title frame = create_frame_config(widget_config) if frame: spec['frame'] = frame return {'name': generate_id(), 'spec': spec, 'queries': create_widget_queries(config, datasets)} def create_advanced_pivot_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced pivot widget - currently redirects to simple version.""" return create_pivot_widget(config, datasets) def create_advanced_text_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create advanced text widget - currently redirects to simple version.""" return create_text_widget(config, datasets) # New Missing Widget Types Implementation def create_symbol_map_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create symbol map widget with latitude/longitude encoding.""" widget_config = config.get('config', {}) encodings = {} # Required latitude/longitude encodings if 'latitude_field' in widget_config: encodings['latitude'] = { 'fieldName': widget_config['latitude_field'], 'displayName': widget_config.get('latitude_display_name', 'Latitude'), } if 'longitude_field' in widget_config: encodings['longitude'] = { 'fieldName': widget_config['longitude_field'], 'displayName': widget_config.get('longitude_display_name', 'Longitude'), } # Optional size encoding if 'size_field' in widget_config: encodings['size'] = { 'fieldName': widget_config['size_field'], 'scale': {'type': 'quantitative'}, 'displayName': widget_config.get('size_display_name', widget_config['size_field']), } # Optional color encoding if 'color_field' in widget_config: color_scale_type = widget_config.get('color_scale_type', 'categorical') encodings['color'] = { 'fieldName': widget_config['color_field'], 'scale': create_color_scale(color_scale_type, widget_config), 'displayName': widget_config.get('color_display_name', widget_config['color_field']), } return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['symbol-map'], 'widgetType': 'symbol-map', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_funnel_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create funnel widget with x/y encodings matching working examples. Funnel charts visualize conversion rates through sequential stages, perfect for sales pipelines, user journeys, or any process with drop-off between steps. Funnel charts require both value_field (quantitative) and stage_field (categorical). If stage_field is missing, we'll attempt to infer it from available fields. Args: config: Widget configuration with value_field and stage_field (or alternatives) datasets: Available datasets for query generation Returns: Complete funnel widget specification with x/y encodings """ widget_config = config.get('config', {}) encodings = {} # X encoding (quantitative value) - required for funnel width # This represents the metric being measured at each stage (e.g., user count, revenue) if 'value_field' in widget_config: encodings['x'] = { 'fieldName': widget_config['value_field'], 'scale': {'type': 'quantitative'}, 'displayName': widget_config.get('value_display_name', widget_config['value_field']), } # Y encoding (categorical stage) - required for funnel levels # This represents the sequential stages (e.g., "Awareness", "Interest", "Purchase") if 'stage_field' in widget_config: encodings['y'] = { 'fieldName': widget_config['stage_field'], 'scale': {'type': 'categorical'}, 'displayName': widget_config.get('stage_display_name', widget_config['stage_field']), } else: # If stage_field is missing, try to infer from other common field names # This handles cases where only value_field is provided potential_stage_fields = [] # Check for common categorical field names in the config for field_key in ['category_field', 'x_field', 'color_field']: if field_key in widget_config and field_key != 'value_field': potential_stage_fields.append(widget_config[field_key]) # Use the first available categorical field as stage_field if potential_stage_fields: stage_field = potential_stage_fields[0] encodings['y'] = { 'fieldName': stage_field, 'scale': {'type': 'categorical'}, 'displayName': widget_config.get('stage_display_name', stage_field), } # Add label configuration as shown in working examples (shows values on funnel) encodings['label'] = {'show': True} # Validation: Ensure both x and y encodings are present for funnel charts if 'x' not in encodings: raise ValueError('Funnel chart requires a value_field for the x (quantitative) encoding') if 'y' not in encodings: raise ValueError( 'Funnel chart requires a stage_field for the y (categorical) encoding. ' "Please provide either 'stage_field', 'category_field', 'x_field', or 'color_field' " 'in the widget configuration for the categorical dimension.' ) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['funnel'], 'widgetType': 'funnel', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_combo_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create combo chart with multiple y-axis fields and chart types.""" widget_config = config.get('config', {}) encodings = {} # X-axis encoding if 'x_field' in widget_config: x_scale_type = widget_config.get('x_scale_type', 'categorical') encodings['x'] = { 'fieldName': widget_config['x_field'], 'scale': {'type': x_scale_type}, 'displayName': widget_config.get('x_display_name', widget_config['x_field']), } # Y-axis with multiple fields and chart types if 'y_fields' in widget_config: y_fields = [] for field_config in widget_config['y_fields']: y_fields.append( { 'fieldName': field_config['field'], 'displayName': field_config.get('displayName', field_config['field']), 'chartType': field_config.get('chartType', 'bar'), } ) encodings['y'] = {'fields': y_fields, 'scale': {'type': 'quantitative'}} # Optional secondary y-axis if 'y2_fields' in widget_config: y2_fields = [] for field_config in widget_config['y2_fields']: y2_fields.append( { 'fieldName': field_config['field'], 'displayName': field_config.get('displayName', field_config['field']), 'chartType': field_config.get('chartType', 'line'), } ) encodings['y2'] = {'fields': y2_fields, 'scale': {'type': 'quantitative'}} return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['combo'], 'widgetType': 'combo', 'encodings': encodings, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), } def create_range_slider_widget(config: Dict, datasets: List[Dict]) -> Dict[str, Any]: """Create range slider filter widget.""" widget_config = config.get('config', {}) fields = [] if 'field' in widget_config: field_config = { 'fieldName': widget_config['field'], 'displayName': widget_config.get('display_name', widget_config['field']), 'dataType': widget_config.get('data_type', 'integer'), } # Add min/max/step if provided if 'min_value' in widget_config: field_config['min'] = widget_config['min_value'] if 'max_value' in widget_config: field_config['max'] = widget_config['max_value'] if 'step' in widget_config: field_config['step'] = widget_config['step'] fields.append(field_config) return { 'name': generate_id(), 'spec': { 'version': WIDGET_VERSIONS['range-slider'], 'widgetType': 'range-slider', 'encodings': {'fields': fields}, 'frame': create_frame_config(widget_config), }, 'queries': create_widget_queries(config, datasets), }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PulkitXChadha/awesome-databricks-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server