Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/duneanalytics/cli/cmd/query"
"github.com/duneanalytics/cli/cmd/sim"
"github.com/duneanalytics/cli/cmd/usage"
"github.com/duneanalytics/cli/cmd/visualization"
"github.com/duneanalytics/cli/cmd/whoami"
"github.com/duneanalytics/cli/cmdutil"
"github.com/duneanalytics/cli/tracking"
Expand Down Expand Up @@ -121,6 +122,7 @@ func init() {
rootCmd.AddCommand(usage.NewUsageCmd())
rootCmd.AddCommand(whoami.NewWhoAmICmd())
rootCmd.AddCommand(sim.NewSimCmd())
rootCmd.AddCommand(visualization.NewVisualizationCmd())
}

// Execute runs the root command via Fang.
Expand Down
145 changes: 145 additions & 0 deletions cmd/visualization/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package visualization

import (
"encoding/json"
"fmt"

"github.com/duneanalytics/cli/cmdutil"
"github.com/duneanalytics/cli/output"
"github.com/duneanalytics/duneapi-client-go/models"
"github.com/spf13/cobra"
)

func newCreateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a new visualization on an existing query",
Long: `Create a visualization attached to an existing saved query.

IMPORTANT: Visualizations can only be attached to saved (non-temporary) queries.
If you created the query with --temp, you must recreate it without --temp first.

The --options flag is required for a working visualization. Without proper options,
the visualization will fail to render. The options format depends on the
visualization type.

Visualization types: chart, table, counter, pivot, cohort, funnel, choropleth,
sankey, sunburst_sequence, word_cloud.

For chart type, set globalSeriesType to: column, line, area, scatter, or pie.

COUNTER (simplest — shows a single number):
--type counter --options '{
"counterColName": "<column>",
"rowNumber": 1,
"stringDecimal": 0,
"stringPrefix": "",
"stringSuffix": "",
"counterLabel": "My Label",
"coloredPositiveValues": false,
"coloredNegativeValues": false
}'

TABLE (displays query results as a table):
--type table --options '{
"itemsPerPage": 25,
"columns": [
{"name": "<column>", "title": "Display Name", "type": "normal",
"alignContent": "left", "isHidden": false}
]
}'

COLUMN/LINE/AREA/SCATTER CHART:
--type chart --options '{
"globalSeriesType": "line",
"sortX": true,
"legend": {"enabled": true},
"series": {"stacking": null},
"xAxis": {"title": {"text": "Date"}},
"yAxis": [{"title": {"text": "Value"}}],
"columnMapping": {"<x_column>": "x", "<y_column>": "y"},
"seriesOptions": {
"<y_column>": {"type": "line", "yAxis": 0, "zIndex": 0}
}
}'

PIE CHART:
--type chart --options '{
"globalSeriesType": "pie",
"sortX": true,
"showDataLabels": true,
"columnMapping": {"<category_column>": "x", "<value_column>": "y"},
"seriesOptions": {
"<value_column>": {"type": "pie", "yAxis": 0, "zIndex": 0}
}
}'

Column names in options must match actual query result columns exactly.

Examples:
# Counter showing row count
dune viz create --query-id 12345 --name "Total Count" --type counter \
--options '{"counterColName":"count","rowNumber":1,"stringDecimal":0}'

# Line chart of daily volume
dune viz create --query-id 12345 --name "Daily Volume" --type chart \
--options '{"globalSeriesType":"line","sortX":true,"columnMapping":{"day":"x","volume":"y"},"seriesOptions":{"volume":{"type":"line","yAxis":0,"zIndex":0}},"xAxis":{"title":{"text":"Day"}},"yAxis":[{"title":{"text":"Volume"}}],"legend":{"enabled":true},"series":{"stacking":null}}'

# Simple table
dune viz create --query-id 12345 --name "Results" --type table \
--options '{"itemsPerPage":25,"columns":[{"name":"address","title":"Address","type":"normal","alignContent":"left","isHidden":false},{"name":"balance","title":"Balance","type":"normal","alignContent":"right","isHidden":false}]}'`,
RunE: runCreate,
}

cmd.Flags().Int("query-id", 0, "ID of the query to attach the visualization to (required)")
cmd.Flags().String("name", "", "visualization name, max 300 characters (required)")
cmd.Flags().String("type", "table", "visualization type: chart, table, counter")
cmd.Flags().String("description", "", "visualization description, max 1000 characters")
cmd.Flags().String("options", "", `visualization options JSON (required for working visualizations, see --help for format per type)`)
_ = cmd.MarkFlagRequired("query-id")
_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("options")
output.AddFormatFlag(cmd, "text")

return cmd
}

func runCreate(cmd *cobra.Command, _ []string) error {
client := cmdutil.ClientFromCmd(cmd)

queryID, _ := cmd.Flags().GetInt("query-id")
name, _ := cmd.Flags().GetString("name")
vizType, _ := cmd.Flags().GetString("type")
description, _ := cmd.Flags().GetString("description")
optionsStr, _ := cmd.Flags().GetString("options")

var options map[string]any
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
return fmt.Errorf("invalid --options JSON: %w", err)
}

resp, err := client.CreateVisualization(models.CreateVisualizationRequest{
QueryID: queryID,
Name: name,
Type: vizType,
Description: description,
Options: options,
})
if err != nil {
return err
}

url := fmt.Sprintf("https://dune.com/embeds/%d/%d", queryID, resp.ID)

w := cmd.OutOrStdout()
switch output.FormatFromCmd(cmd) {
case output.FormatJSON:
return output.PrintJSON(w, map[string]any{
"id": resp.ID,
"url": url,
})
default:
fmt.Fprintf(w, "Created visualization %d on query %d\n%s\n", resp.ID, queryID, url)
return nil
}
}
18 changes: 18 additions & 0 deletions cmd/visualization/visualization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package visualization

import "github.com/spf13/cobra"

func NewVisualizationCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "visualization",
Aliases: []string{"viz"},
Short: "Create and manage Dune visualizations",
Long: "Create and manage visualizations on Dune queries.\n\n" +
"Visualizations are charts, tables, counters, and other visual representations\n" +
"of query results. Each visualization is attached to a saved query.",
}

cmd.AddCommand(newCreateCmd())

return cmd
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.25.6
require (
github.com/amplitude/analytics-go v1.3.0
github.com/charmbracelet/fang v0.4.4
github.com/duneanalytics/duneapi-client-go v0.4.5
github.com/duneanalytics/duneapi-client-go v0.4.6
github.com/modelcontextprotocol/go-sdk v1.4.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duneanalytics/duneapi-client-go v0.4.5 h1:LSSW2B2KG/OIXWuWHfVpOhsvuuR/lw4nJ7uyYE6NHB4=
github.com/duneanalytics/duneapi-client-go v0.4.5/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE=
github.com/duneanalytics/duneapi-client-go v0.4.6 h1:ZjW/H86Da6RY3K9rQFUV3pU1w8kTVlgSGH3k4v3VP7Q=
github.com/duneanalytics/duneapi-client-go v0.4.6/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down
Loading