Introduction

You’d like to visualize some stock data using Go, but after looking at the Go ecosystem you see very little in charting. You find gonum, which has some plotting capabilities, but it generates static charts. It’s 2022, and you’d like to have interactive features such as zooming, panning, and more. You turn to the HTML landscape, and see many more options and decide to take this path. After a short survey, you decide to use plotly.

To get actual stock information, you’ll use Yahoo! finance that lets you download a CSV file with historical information.

Listing 1: Example CSV

Date,Open,High,Low,Close,Adj Close,Volume
2021-01-04,222.529999,223.000000,214.809998,217.690002,215.880432,37130100
2021-01-05,217.259995,218.520004,215.699997,217.899994,216.088669,23823000
2021-01-06,212.169998,216.490005,211.940002,212.250000,210.485641,35930700
2021-01-07,214.039993,219.339996,213.710007,218.289993,216.475418,27694500
2021-01-08,218.679993,220.580002,217.029999,219.619995,217.794388,22956200
2021-01-11,218.470001,218.910004,216.729996,217.490005,215.682083,23031300

Listing one shows an example of a CSV file that was downloaded from Yahoo! finance. You’re going to use only the Date, Close and Volume columns.

Let’s start! First we’ll parse the data

Listing 2: Parsing Time

37 // unmarshalTime unmarshal data in CSV to time
38 func unmarshalTime(data []byte, t *time.Time) error {
39     var err error
40     *t, err = time.Parse("2006-01-02", string(data))
41     return err
42 }

Listing 2 shows parsing of time. In CSV everything is text, and we need to help csvutil to know how to parse time. On line 40, we use time.Parse to parse time from a string in the format 2021-01-11.

Listing 3: Date Types

23 // Row in CSV
24 type Row struct {
25     Date   time.Time
26     Close  float64
27     Volume int
28 }
29 
30 // Table of data
31 type Table struct {
32     Date   []time.Time
33     Price  []float64
34     Volume []int
35 }

Listing 3 shows the data types used in parsing. On line 24, we define Row which corresponds to a row in the CSV file, we define only 3 fields for the columns we’re interested in. Close is used to represent the final stock price for that day.

On line 31, we define the output type - Table. We have three columns of data: Data, Price (from the Close column in the CSV), and Volume.

Listing 4: Parsing Data

44 // parseData parses data from r and returns a table with columns filled
45 func parseData(r io.Reader) (Table, error) {
46     dec, err := csvutil.NewDecoder(csv.NewReader(r))
47     if err != nil {
48         return Table{}, err
49     }
50     dec.Register(unmarshalTime)
51 
52     var table Table
53     for {
54         var row Row
55         err := dec.Decode(&row)
56 
57         if err == io.EOF {
58             break
59         }
60 
61         if err != nil {
62             return Table{}, err
63         }
64 
65         table.Date = append(table.Date, row.Date)
66         table.Price = append(table.Price, row.Close)
67         table.Volume = append(table.Volume, row.Volume)
68     }
69 
70     return table, nil
71 }

Listing 4 shows how to parse the data. On line 46, we create a new csvutil.Decoder and on line 50, we register the unmarshalTime function to handle time.Time fields. On line 52, we construct the output value table. On line 53, we start iterating over the input, on line 54, we construct a new Row and on line 55, we decode the current line in the CSV into the Row. On line 57, we check for the end of input and on line 61, we check for other errors. On lines 65-67, we append the values from the current row to the respective columns. Finally on line 70, we return the parsed input.

Note: To test this code, I’ve downloaded a CSV file one and used parseData on the opened file. This make the development cycle faster and also reduces the chances you’ll be banned from hitting the API too frequently.

Once we have parsed the data, we can get it from Yahoo! finance.

Listing 5a: Building the URL

73 // buildURL builds URL for downloading CSV from Yahoo! finance
74 func buildURL(symbol string, start, end time.Time) string {
75     u := fmt.Sprintf("https://query1.finance.yahoo.com/v7/finance/download/%s", url.PathEscape(symbol))
76     v := url.Values{
77         "period1":  {fmt.Sprintf("%d", start.Unix())},
78         "period2":  {fmt.Sprintf("%d", end.Unix())},
79         "interval": {"1d"},
80         "events":   {"history"},
81     }
82 
83     return fmt.Sprintf("%s?%s", u, v.Encode())
84 }

Listing 5a shows how to build the URL to fetch the CSV. The final URL looks like:

Listing 5b: URL

https://query1.finance.yahoo.com/v7/finance/download/MSFT?period1=1609286400&period2=1640822400&interval=1d&events=history

On line 75, we use fmt.Sprintf and url.PathEscape to create the first part of the URL (up to ?). On lines 76 to 80, we create the query part of the URL using a url.Values. Finally on line 83, we return the full URL.

Note: url.PathEscape handle will convert “A/B” to “A%2FB” which is valid as part of URL path.

Listing 6: Getting the Data

86 // stockData returns stock data from Yahoo! finance
87 func stockData(symbol string, start, end time.Time) (Table, error) {
88     u := buildURL(symbol, start, end)
89     resp, err := http.Get(u)
90     if err != nil {
91         return Table{}, err
92     }
93     if resp.StatusCode != http.StatusOK {
94         return Table{}, fmt.Errorf("%s", resp.Status)
95     }
96     defer resp.Body.Close()
97 
98     return parseData(resp.Body)
99 }

Listing 6 shows how we get the data. On line 88, we build the URL and on line 89, we make an HTTP GET request. On lines 90 and 93, we check for errors and finally on line 98, we return the result of parseData on the response body.

Now that we get and parse our data, we can build our web server.

Listing 7: index.html

01 <!DOCTYPE html>
02 <html>
03     <head>
04         <title>Stocks</title>
05         <script src="https://cdn.plot.ly/plotly-2.8.3.min.js"></script>
06         <script src="/chart.js"></script>
07         <style>
08                 #symbol {
09                     width: 6em;
10                 }
11                 #chart {
12                     width: 800px;
13                     height: 400px;
14                 }
15         </style>
16     </head>
17     <body>
18         <h3>Stocks</h3>
19         Symbol: <input id="symbol"> <button id="generate">Generate</button>
20         <hr />
21         <div id="chart"></div>
22     </body>
23 </html>

Listing 7 shows the index.html. On line 05, we load the plotly JavaScript library and on line 06, we load our JavaScript code. On line 19, we define the input control for the symbol (stock) and on line 21, we have the div that plotly will draw the chart on.

Listing 8: chart.js

01 async function updateChart() {
02     let symbol = document.getElementById('symbol').value;
03     let resp = await fetch('/data?symbol=' + symbol);
04     let reply = await resp.json(); 
05     Plotly.newPlot('chart', reply.data, reply.layout);
06 }
07 
08 document.addEventListener('DOMContentLoaded', function () {
09     document.getElementById('generate').onclick = updateChart;
10 });

Listing 8 shows the JavaScript code. On line 01, we define a function to update the chart. On line 02, we get the symbol name from the HTML input. On line 03, we call our server to get the data and on line 04, we parse the JSON. Finally on line 05, we use plotly to generate a new chart.

On lines 08-10, we hook the “Generate” button click to call updateChart.

Listing 9: Data Handler

101 // dataHandler returns JSON data for symbol
102 func dataHandler(w http.ResponseWriter, r *http.Request) {
103     symbol := r.URL.Query().Get("symbol")
104     if symbol == "" {
105         http.Error(w, "empty symbol", http.StatusBadRequest)
106         return
107     }
108     log.Printf("data: %q", symbol)
109     start := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
110     end := time.Date(2021, time.December, 31, 0, 0, 0, 0, time.UTC)
111     table, err := stockData(symbol, start, end)
112     if err != nil {
113         log.Printf("get %q: %s", symbol, err)
114         http.Error(w, "can't fetch data", http.StatusInternalServerError)
115         return
116     }
117 
118     if err := tableJSON(symbol, table, w); err != nil {
119         log.Printf("table: %s", err)
120     }
121 }

Listing 9 shows the data handler. On line 103, we extract the symbol from the HTTP symbol parameter. On line 111, we call stockData to get the stock data and finally on line 118, we convert the Table output to JSON.

Listing 10: Table as JSON

123 // tableJSON writes table data as JSON into w
124 func tableJSON(symbol string, table Table, w io.Writer) error {
125     reply := map[string]interface{}{
126         "data": []map[string]interface{}{
127             {
128                 "x":    table.Date,
129                 "y":    table.Price,
130                 "name": "Price",
131                 "type": "scatter",
132             },
133             {
134                 "x":     table.Date,
135                 "y":     table.Volume,
136                 "name":  "Volume",
137                 "type":  "bar",
138                 "yaxis": "y2",
139             },
140         },
141         "layout": map[string]interface{}{
142             "title": symbol,
143             "grid": map[string]int{
144                 "rows":    2,
145                 "columns": 1,
146             },
147         },
148     }
149 
150     return json.NewEncoder(w).Encode(reply)
151 }

Listing 10 shows how we convert a Table to JSON used by our JavaScript code. On lines 125-139, we define a map that will be marshalled to JSON. On lines 126 to 140, we define two traces that contain the plotting data. On line 131, we specify that the first trace is a scatter (line) plot and on line 137, we specify the second trace is a bar plot. On line 138, we tell plotly that the var plot of the volume should have its own y axis. One lines 141-147, we define the layout of the plot, we want two rows and a single column. Finally on line 150, we use a json.ENcoder to encode this struct.

Listing 11: Embedding Files

18 var (
19     //go:embed chart.js index.html
20     staticFS embed.FS
21 )

Listing 11 shows how we embed the non-Go files in our server using the embed package. On line 19, we use a go:embed directive to embed several files and on line 20, we define staticFS that implements fs.FS interface.

Listing 12: Running The Server

153 func main() {
154     http.Handle("/", http.FileServer(http.FS(staticFS)))
155     http.HandleFunc("/data", dataHandler)
156 
157     if err := http.ListenAndServe(":8080", nil); err != nil {
158         log.Fatal(err)
159     }
160 }

Listing 12 shows the main function. On line 154, we use an http.FileServer to server the embedded files and on line 155, we route /data to the dataHandler. Finally on line 157, we run the server on port 8080.

The final result look like the below image:

Conclusion

In about 160 lines of code we managed to create an interactive application that displays stock information. plotly is a very mature library with a lot of features the documentation is great. I usually start with a chart that looks similar to what I want to display and adjust to my needs.

If your code is in a database, you might need zero code to display it. Products such as grafana, Google Data Studio and others allow you to create cool dashboards with very little effort.

What cool visualizations did you create with Go? Let me know

Trusted by top technology companies

We've built our reputation as educators and bring that mentality to every project. When you partner with us, your team will learn best practices and grow along the way.

30,000+

Engineers Trained

1,000+

Companies Worldwide

12+

Years in Business