Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions runtime/ai/create_chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,29 @@ func validateChartFields(chartType string, spec map[string]any, mvSpec *runtimev
return fmt.Errorf("invalid measure fields: %w", err)
}
}
// Optional enum-valued funnel fields. Cross-validation between breakdownMode
// and color (e.g. color "stage" requires dimension mode) is enforced by the
// chart renderer, which falls back to sensible defaults; we only reject typos here.
if breakdownMode, ok := spec["breakdownMode"]; ok {
if err := validateEnum("breakdownMode", breakdownMode, []string{"dimension", "measures"}); err != nil {
return err
}
}
if mode, ok := spec["mode"]; ok {
if err := validateEnum("mode", mode, []string{"width", "order"}); err != nil {
return err
}
}
if color, ok := spec["color"]; ok {
if err := validateEnum("color", color, []string{"stage", "measure", "name", "value"}); err != nil {
return err
}
}
if percentMode, ok := spec["percentMode"]; ok {
if err := validateEnum("percentMode", percentMode, []string{"top", "previous"}); err != nil {
return err
}
}

case "donut_chart", "pie_chart":
if colorField, ok := pathutil.GetPath(spec, "color.field"); ok {
Expand Down Expand Up @@ -336,6 +359,20 @@ func validateField(availableFields map[string]bool, field any) error {
return nil
}

// validateEnum checks that value is a string and one of the allowed options.
func validateEnum(name string, value any, allowed []string) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("%q must be a string", name)
}
for _, a := range allowed {
if str == a {
return nil
}
}
return fmt.Errorf("%q must be one of %v, got %q", name, allowed, str)
}

// validateFieldsArray validates an array of field names
func validateFieldsArray(availableFields map[string]bool, fields any) error {
fieldsArray, ok := fields.([]any)
Expand Down Expand Up @@ -852,6 +889,14 @@ number_of_commits: measure
### 7. Funnel Chart (` + "`funnel_chart`" + `)
**Use for:** Showing flow through a process with decreasing values at each stage or measure

**Funnel-specific fields:**
- ` + "`breakdownMode`" + ` (required): ` + "`\"dimension\"`" + ` (one measure split by a stage dimension) or ` + "`\"measures\"`" + ` (multiple measures, one per stage)
- ` + "`mode`" + `: ` + "`\"width\"`" + ` (bar width proportional to value, default) or ` + "`\"order\"`" + ` (bars shrink by fixed ratio in rank order)
- ` + "`color`" + `:
- In ` + "`dimension`" + ` mode: ` + "`\"stage\"`" + ` (distinct color per stage) or ` + "`\"measure\"`" + ` (gradient by value)
- In ` + "`measures`" + ` mode: ` + "`\"name\"`" + ` (distinct color per measure) or ` + "`\"value\"`" + ` (gradient by value)
- ` + "`percentMode`" + `: ` + "`\"top\"`" + ` (default; on-bar % is relative to the top stage) or ` + "`\"previous\"`" + ` (on-bar % is relative to the prior stage). The tooltip always shows both, regardless of this setting. Choose ` + "`\"previous\"`" + ` when the user asks about stage-to-stage drop-off or step conversion; choose ` + "`\"top\"`" + ` when they ask about overall conversion from the entry point.

Example Specification with 1 dimension and 1 measure breakdown

Field details:
Expand All @@ -875,6 +920,7 @@ total_users_measure: measure
"type": "quantitative"
},
"mode": "width",
"percentMode": "top",
"stage": {
"field": "stage",
"limit": 15,
Expand All @@ -884,7 +930,7 @@ total_users_measure: measure
}
` + "```" + `

Example Specification with multiple measures breakdown
Example Specification with multiple measures breakdown and stage-to-stage percentages

Field details:
bids: metrics_view
Expand All @@ -910,7 +956,8 @@ impressions, video_starts, video_completes: measures
]
},
"metrics_view": "bids",
"mode": "width"
"mode": "width",
"percentMode": "previous"
}
}
` + "```" + `
Expand Down
9 changes: 8 additions & 1 deletion runtime/ai/instructions/data/resources/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ funnel_chart:
breakdownMode: dimension
color: stage
mode: width
percentMode: top
stage:
field: funnel_stage
type: nominal
Expand All @@ -683,7 +684,7 @@ funnel_chart:
type: quantitative
```

**With multiple measures breakdown:**
**With multiple measures breakdown and step-to-step percentages:**

```yaml
funnel_chart:
Expand All @@ -692,6 +693,7 @@ funnel_chart:
breakdownMode: measures
color: value
mode: width
percentMode: previous
measure:
field: impressions
type: quantitative
Expand All @@ -706,6 +708,11 @@ funnel_chart:
- `breakdownMode: dimension` with `color: stage` (different colors per stage) or `color: measure` (similar colors by value)
- `breakdownMode: measures` with `color: name` (different colors per measure) or `color: value` (similar colors by value)

**Percent mode (`percentMode`):**
- `top` (default): the on-bar percentage label is each stage's value as a share of the top stage. Use for overall conversion ("how many of the entry-point users reached this stage").
- `previous`: the on-bar percentage label is each stage's value as a share of the immediately prior stage. Use for stage-to-stage drop-off ("what fraction of the previous stage continued").
- The tooltip always shows both `% of top` and `% of previous` regardless of this setting, so users can disambiguate without the author surfacing both.

### Pivot

Create pivot tables with row and column dimensions:
Expand Down
35 changes: 34 additions & 1 deletion runtime/canvas/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,22 @@ func validateFunnelChart(props map[string]any, metricsViews map[string]*runtimev
}
}

return validateOptionalDimensionField(mv, mvn, props, "stage.field")
if err := validateOptionalDimensionField(mv, mvn, props, "stage.field"); err != nil {
return err
}

// Optional enum-valued funnel fields. The renderer falls back to defaults for
// unknown values, but we reject typos here so authors get a clear error.
if err := validateOptionalStringEnum(props, "breakdownMode", []string{"dimension", "measures"}); err != nil {
return err
}
if err := validateOptionalStringEnum(props, "mode", []string{"width", "order"}); err != nil {
return err
}
if err := validateOptionalStringEnum(props, "color", []string{"stage", "measure", "name", "value"}); err != nil {
return err
}
return validateOptionalStringEnum(props, "percentMode", []string{"top", "previous"})
}

// validateHeatmap validates properties for heatmap.
Expand Down Expand Up @@ -476,6 +491,24 @@ func getOptionalPathString(props map[string]any, path string) (string, bool, err
return s, true, nil
}

// validateOptionalStringEnum validates that an optional string field at the given
// path, if present, equals one of the allowed values.
func validateOptionalStringEnum(props map[string]any, path string, allowed []string) error {
value, ok, err := getOptionalPathString(props, path)
if err != nil {
return err
}
if !ok {
return nil
}
for _, a := range allowed {
if value == a {
return nil
}
}
return fmt.Errorf("renderer property %q must be one of %v, got %q", path, allowed, value)
}

// metricsViewHasDimension returns true if the metrics view has a dimension with the given name.
func metricsViewHasDimension(mv *runtimev1.MetricsViewSpec, fieldName string) bool {
for _, d := range mv.Dimensions {
Expand Down
5 changes: 5 additions & 0 deletions runtime/metricsview/charts_json_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ const ChartsJSONSchema = `{
"type": "string",
"description": "Display mode for the chart."
},
"percentMode": {
"type": "string",
"enum": ["top", "previous"],
"description": "For funnel charts: reference for the on-bar percentage label. 'top' shows each stage as % of the top stage; 'previous' shows each stage as % of the prior stage. Tooltip always shows both regardless of this setting."
},
"show_data_labels": {
"type": "boolean",
"description": "Whether to show data labels on the chart."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export class FunnelChartComponent extends BaseChart<FunnelCanvasChartSpec> {
],
},
},
percentMode: {
type: "switcher_tab",
label: "Percent of",
meta: {
default: "top",
options: [
{ label: "Top", value: "top" },
{ label: "Previous", value: "previous" },
],
},
},
};

constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) {
Expand Down Expand Up @@ -258,6 +269,7 @@ export class FunnelChartComponent extends BaseChart<FunnelCanvasChartSpec> {
mode: "width",
color: "stage",
breakdownMode: "dimension",
percentMode: "top",
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getFilterWithNullHandling } from "../query-util";
export type FunnelMode = "width" | "order";
export type FunnelColorMode = "stage" | "measure" | "name" | "value";
export type FunnelBreakdownMode = "dimension" | "measures";
export type FunnelPercentMode = "top" | "previous";

export type FunnelChartSpec = {
metrics_view: string;
Expand All @@ -38,6 +39,7 @@ export type FunnelChartSpec = {
stage?: FieldConfig<"nominal">;
mode?: FunnelMode;
color?: FunnelColorMode;
percentMode?: FunnelPercentMode;
};

export type FunnelChartDefaultOptions = {
Expand Down
Loading
Loading