├── .gitignore
├── 01-svg.md
├── 02-highcharts.md
├── 03-rails-api.md
├── 04-rails-api-highcharts.md
├── README.md
├── index.html
└── js
├── chart.js
├── chart2.js
└── chart3.js
/.gitignore:
--------------------------------------------------------------------------------
1 | tutorial.rb
2 | _index.html
3 | csvs
4 | beer-recommender
5 |
--------------------------------------------------------------------------------
/01-svg.md:
--------------------------------------------------------------------------------
1 | #Making graphs manually with SVG
2 |
3 | SVG, or Scalable Vector Graphics, an XML-based vector graphics system for the web allows for a number of benefits: SVG images can scale and zoom without degradation, they can be written with anything that supports HTML (though they are often drawn with an imaging program) and they can even be searched and indexed!
4 |
5 | For purposes of our discussion here, we are going to stick to a discussion of basic shapes like circles and rectangles, even though SVG can be used for much more. You can read more about SVG [here](http://en.wikipedia.org/wiki/Scalable_Vector_Graphics).
6 |
7 | So how do we make images appear on an `html` page?
8 |
9 | Let's make a basic html index page called `index.html`, which we will be updating:
10 |
11 | ```html
12 |
13 |
14 |
My First SVG
15 |
16 |
17 | ```
18 |
19 | This should be pretty straight forward. We're just creating an `index.html` file that results in the following output when opening up in the browser:
20 |
21 | 
22 |
23 | Great. So we'll start updating our `index.html` file with an SVG image. Let's try something simple like a line. To be able to do that, let's stop for a second and discuss the SVG coordinate system. The SVG coordinate system is a little different from the *x-y* *Cartesian* coordinate system we are familiar with. You probably saw lots of graphs like these in high school:
24 |
25 | 
26 |
27 | The coordinate `(0,0)` is at the bottom left of graph, and each step forward in *x* would be a move directionally *right* and each step forward in *y* would be a move directionally *up*.
28 |
29 | The SVG coordinate system, which we'll call the SVG *canvas*, is a bit different. While *x* refers to horizontal direction and *y* refers to vertical direction in the SVG canvas (much like the Cartesian system), the coordinate `(0,0)` differs in that it is at the top left of the coordinate system:
30 |
31 | 
32 |
33 | Why the coordinate system starts at the top left corner is not incredibly important, but keep in mind that the *y* coordinate is reversed compared to the Cartesian system (if you are interested in reading more about the *why*, see this [link](http://gamedev.stackexchange.com/a/83571) for an explanation). This means that *y* moves vertically down on the screen when its value goes up.
34 |
35 | So let's actually draw something! First, we have to create an SVG canvas for us in our `index.html` file:
36 |
37 | ```html
38 |
39 |
40 |
My First SVG
41 |
43 |
44 |
45 | ```
46 |
47 | What this says is we are creating a canvas with width `100` and height `100`. Within that canvas, we can draw whatever we want. A *canvas* here is just like a canvas an artist might use: a physical area to draw in.
48 |
49 | A line is probably one of the simpler things we can draw:
50 |
51 | ```html
52 |
53 |
54 |
My First SVG
55 |
58 |
59 |
60 | ```
61 |
62 | Before we discuss the code, let's see what it looks like on the screen:
63 |
64 | 
65 |
66 | Our code is pretty straightforward. One point is at `(0, 100)` and the line is drawn from there to `(100, 100)`. The rest of the code is for the color (red) and the thickness of the line. What if we wanted to draw a line from the top left corner of the canvas to the bottom right? First, we have to figure out the coordinates. The top left corner is `(0, 0)` and the bottom right corner is `(100, 100)`. Let's try that in our code and see how it looks:
67 |
68 | ```html
69 |
70 |
71 |
My First SVG
72 |
75 |
76 |
77 | ```
78 |
79 | The above code translates into the following:
80 |
81 | 
82 |
83 | Great! So we know how to draw a line, but can we do more? Let's try a circle. SVG is pretty convenient. We just need to define a circle by location, radius and color and we're good to go:
84 |
85 | ```html
86 |
87 |
88 |
My First SVG
89 |
92 |
93 |
94 | ```
95 |
96 | We are defining a circle with *x* and *y* locations at point `(50, 50)` on the canvas, with a radius of 20, a border of black, stroke width of 4 and fill of the color blue, which looks like this:
97 |
98 | 
99 |
100 | Let's add a second circle. Stop for a second and think about how we can do that.
101 |
102 | Yep, it's that easy. Just another line of code:
103 |
104 | ```html
105 |
106 |
107 |
My First SVG
108 |
112 |
113 |
114 | ```
115 |
116 | And now on the screen:
117 |
118 | 
119 |
120 | Nice! Now we have two beautiful circles on our canvas. We can add multiple circles but you'd basically have to draw each additional shape manually to generate the image:
121 |
122 | ```html
123 |
130 | ```
131 |
132 | For a scatterplot, this could result in ridiculously long code. We could try to be clever and write some Ruby code to generate the html code for us based on some inputs (just using random inputs here to make a point):
133 |
134 | ```ruby
135 | points = Array.new
136 |
137 | 25.times do
138 | x = rand(100)
139 | y = rand(100)
140 | points << [x, y]
141 | end
142 |
143 | svg_string = ""
144 | points.each do |point|
145 | svg_string += "\n"
146 | end
147 | ```
148 |
149 | If we stick the string generated in `svg_string` in our `index.html` file, it would look like this:
150 |
151 | ```html
152 |
153 |
154 |
My First SVG
155 |
184 |
185 |
186 | ```
187 |
188 | That would generate the following scatter plot:
189 |
190 | 
191 |
192 | It's a start! But that's a whole lot of work just to generate a single scatter plot. Plus it doesn't even look that nice. What if I need to make a bunch of scatter plots or want to style and color them differently? There has to be an easier way! We'll look at that easier way in the next section on Highcharts, a Javascript charting library.
193 |
194 | The Highcharts Javascript Charting Library: [Part 2](02-highcharts.md)
195 |
--------------------------------------------------------------------------------
/02-highcharts.md:
--------------------------------------------------------------------------------
1 | #Enter Highcharts
2 |
3 | [Highcharts](http://www.highcharts.com) is a Javascript library that simplifies charting data dramatically. It's a great way to get started with generating some nice looking charts. We'll be following the Highcharts installation link [here](http://www.highcharts.com/docs/getting-started/installation). It's got a pretty good explanation of how to get started with Highcharts. First, let's setup our `index.html` file to *require* highcharts:
4 |
5 | ```html
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ```
18 |
19 | Now, let's add the code per the discussion [here](http://www.highcharts.com/docs/getting-started/your-first-chart):
20 |
21 | `index.html`:
22 |
23 | ```html
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | ```
38 |
39 | We also create a folder called `js` where we store our javascript scripts. We'll name ours `chart.js` and place the code from the Highcharts [site]
40 |
41 | `js/chart.js`:
42 |
43 | ```javascript
44 | $(function () {
45 | $('#container').highcharts({
46 | chart: {
47 | type: 'bar'
48 | },
49 | title: {
50 | text: 'Fruit Consumption'
51 | },
52 | xAxis: {
53 | categories: ['Apples', 'Bananas', 'Oranges']
54 | },
55 | yAxis: {
56 | title: {
57 | text: 'Fruit eaten'
58 | }
59 | },
60 | series: [{
61 | name: 'Jane',
62 | data: [1, 0, 4]
63 | }, {
64 | name: 'John',
65 | data: [5, 7, 3]
66 | }]
67 | });
68 | });
69 | ```
70 |
71 | This is what our directory structure should look like:
72 |
73 | ```
74 | .
75 | ├── index.html
76 | ├── js
77 | │ └── chart.js
78 |
79 | 1 directory, 2 files
80 | ```
81 |
82 | If we load up `index.html`, we should see the following chart:
83 |
84 | 
85 |
86 | Let's look back above at our `script.js` file and see what's going on. `series` determines the data that we see in our chart. If we change our `series` in our `chart.js` file as follows with some MMA fighters and values, let's see how our chart is updated.
87 |
88 | ```javascript
89 | series: [{
90 | name: 'Ronda Rousey',
91 | data: [5, 5, 5]
92 | }, {
93 | name: 'Jon Jones',
94 | data: [8, 7, 8]
95 | }]
96 | ```
97 |
98 | And the updated chart:
99 |
100 | 
101 |
102 | Seems pretty straight forward. Highcharts seems like a good option. Can we use it for a scatter plot? Navigating to [jsfiddle](http://jsfiddle.net/gh/get/jquery/1.9.1/highslide-software/highcharts.com/tree/master/samples/highcharts/demo/scatter/) on the Highcharts [demo](http://www.highcharts.com/demo/scatter) site can help us out here. Let's take a look at the code provided and see if we can adjust it for the random scatter plot we looked at with SVG:
103 |
104 | ```javascript
105 | $(function () {
106 | $('#container').highcharts({
107 | chart: {
108 | type: 'scatter',
109 | zoomType: 'xy'
110 | },
111 | title: {
112 | text: 'Height Versus Weight of 507 Individuals by Gender'
113 | },
114 | subtitle: {
115 | text: 'Source: Heinz 2003'
116 | },
117 | xAxis: {
118 | title: {
119 | enabled: true,
120 | text: 'Height (cm)'
121 | },
122 | startOnTick: true,
123 | endOnTick: true,
124 | showLastLabel: true
125 | },
126 | yAxis: {
127 | title: {
128 | text: 'Weight (kg)'
129 | }
130 | },
131 | legend: {
132 | layout: 'vertical',
133 | align: 'left',
134 | verticalAlign: 'top',
135 | x: 100,
136 | y: 70,
137 | floating: true,
138 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF',
139 | borderWidth: 1
140 | },
141 | plotOptions: {
142 | scatter: {
143 | marker: {
144 | radius: 5,
145 | states: {
146 | hover: {
147 | enabled: true,
148 | lineColor: 'rgb(100,100,100)'
149 | }
150 | }
151 | },
152 | states: {
153 | hover: {
154 | marker: {
155 | enabled: false
156 | }
157 | }
158 | },
159 | tooltip: {
160 | headerFormat: '{series.name} ',
161 | pointFormat: '{point.x} cm, {point.y} kg'
162 | }
163 | }
164 | },
165 | series: [{
166 | name: 'Female',
167 | color: 'rgba(223, 83, 83, .5)',
168 | data: [[161.2, 51.6], [167.5, 59.0], [159.5, 49.2], [157.0, 63.0], [155.8, 53.6],
169 | [170.0, 59.0], [159.1, 47.6], [166.0, 69.8], [176.2, 66.8], [160.2, 75.2],
170 | [172.5, 55.2], [170.9, 54.2], [172.9, 62.5], [153.4, 42.0], [160.0, 50.0],
171 | [147.2, 49.8], [168.2, 49.2], [175.0, 73.2], [157.0, 47.8], [167.6, 68.8],
172 | [159.5, 50.6], [175.0, 82.5], [166.8, 57.2], [176.5, 87.8], [170.2, 72.8],
173 | [174.0, 54.5], [173.0, 59.8], [179.9, 67.3], [170.5, 67.8], [160.0, 47.0],
174 | [154.4, 46.2], [162.0, 55.0], [176.5, 83.0], [160.0, 54.4], [152.0, 45.8],
175 | [162.1, 53.6], [170.0, 73.2], [160.2, 52.1], [161.3, 67.9], [166.4, 56.6],
176 | [168.9, 62.3], [163.8, 58.5], [167.6, 54.5], [160.0, 50.2], [161.3, 60.3],
177 | [167.6, 58.3], [165.1, 56.2], [160.0, 50.2], [170.0, 72.9], [157.5, 59.8],
178 | [167.6, 61.0], [160.7, 69.1], [163.2, 55.9], [152.4, 46.5], [157.5, 54.3],
179 | [168.3, 54.8], [180.3, 60.7], [165.5, 60.0], [165.0, 62.0], [164.5, 60.3],
180 | [156.0, 52.7], [160.0, 74.3], [163.0, 62.0], [165.7, 73.1], [161.0, 80.0],
181 | [162.0, 54.7], [166.0, 53.2], [174.0, 75.7], [172.7, 61.1], [167.6, 55.7],
182 | [151.1, 48.7], [164.5, 52.3], [163.5, 50.0], [152.0, 59.3], [169.0, 62.5],
183 | [164.0, 55.7], [161.2, 54.8], [155.0, 45.9], [170.0, 70.6], [176.2, 67.2],
184 | [170.0, 69.4], [162.5, 58.2], [170.3, 64.8], [164.1, 71.6], [169.5, 52.8],
185 | [163.2, 59.8], [154.5, 49.0], [159.8, 50.0], [173.2, 69.2], [170.0, 55.9],
186 | [161.4, 63.4], [169.0, 58.2], [166.2, 58.6], [159.4, 45.7], [162.5, 52.2],
187 | [159.0, 48.6], [162.8, 57.8], [159.0, 55.6], [179.8, 66.8], [162.9, 59.4],
188 | [161.0, 53.6], [151.1, 73.2], [168.2, 53.4], [168.9, 69.0], [173.2, 58.4],
189 | [171.8, 56.2], [178.0, 70.6], [164.3, 59.8], [163.0, 72.0], [168.5, 65.2],
190 | [166.8, 56.6], [172.7, 105.2], [163.5, 51.8], [169.4, 63.4], [167.8, 59.0],
191 | [159.5, 47.6], [167.6, 63.0], [161.2, 55.2], [160.0, 45.0], [163.2, 54.0],
192 | [162.2, 50.2], [161.3, 60.2], [149.5, 44.8], [157.5, 58.8], [163.2, 56.4],
193 | [172.7, 62.0], [155.0, 49.2], [156.5, 67.2], [164.0, 53.8], [160.9, 54.4],
194 | [162.8, 58.0], [167.0, 59.8], [160.0, 54.8], [160.0, 43.2], [168.9, 60.5],
195 | [158.2, 46.4], [156.0, 64.4], [160.0, 48.8], [167.1, 62.2], [158.0, 55.5],
196 | [167.6, 57.8], [156.0, 54.6], [162.1, 59.2], [173.4, 52.7], [159.8, 53.2],
197 | [170.5, 64.5], [159.2, 51.8], [157.5, 56.0], [161.3, 63.6], [162.6, 63.2],
198 | [160.0, 59.5], [168.9, 56.8], [165.1, 64.1], [162.6, 50.0], [165.1, 72.3],
199 | [166.4, 55.0], [160.0, 55.9], [152.4, 60.4], [170.2, 69.1], [162.6, 84.5],
200 | [170.2, 55.9], [158.8, 55.5], [172.7, 69.5], [167.6, 76.4], [162.6, 61.4],
201 | [167.6, 65.9], [156.2, 58.6], [175.2, 66.8], [172.1, 56.6], [162.6, 58.6],
202 | [160.0, 55.9], [165.1, 59.1], [182.9, 81.8], [166.4, 70.7], [165.1, 56.8],
203 | [177.8, 60.0], [165.1, 58.2], [175.3, 72.7], [154.9, 54.1], [158.8, 49.1],
204 | [172.7, 75.9], [168.9, 55.0], [161.3, 57.3], [167.6, 55.0], [165.1, 65.5],
205 | [175.3, 65.5], [157.5, 48.6], [163.8, 58.6], [167.6, 63.6], [165.1, 55.2],
206 | [165.1, 62.7], [168.9, 56.6], [162.6, 53.9], [164.5, 63.2], [176.5, 73.6],
207 | [168.9, 62.0], [175.3, 63.6], [159.4, 53.2], [160.0, 53.4], [170.2, 55.0],
208 | [162.6, 70.5], [167.6, 54.5], [162.6, 54.5], [160.7, 55.9], [160.0, 59.0],
209 | [157.5, 63.6], [162.6, 54.5], [152.4, 47.3], [170.2, 67.7], [165.1, 80.9],
210 | [172.7, 70.5], [165.1, 60.9], [170.2, 63.6], [170.2, 54.5], [170.2, 59.1],
211 | [161.3, 70.5], [167.6, 52.7], [167.6, 62.7], [165.1, 86.3], [162.6, 66.4],
212 | [152.4, 67.3], [168.9, 63.0], [170.2, 73.6], [175.2, 62.3], [175.2, 57.7],
213 | [160.0, 55.4], [165.1, 104.1], [174.0, 55.5], [170.2, 77.3], [160.0, 80.5],
214 | [167.6, 64.5], [167.6, 72.3], [167.6, 61.4], [154.9, 58.2], [162.6, 81.8],
215 | [175.3, 63.6], [171.4, 53.4], [157.5, 54.5], [165.1, 53.6], [160.0, 60.0],
216 | [174.0, 73.6], [162.6, 61.4], [174.0, 55.5], [162.6, 63.6], [161.3, 60.9],
217 | [156.2, 60.0], [149.9, 46.8], [169.5, 57.3], [160.0, 64.1], [175.3, 63.6],
218 | [169.5, 67.3], [160.0, 75.5], [172.7, 68.2], [162.6, 61.4], [157.5, 76.8],
219 | [176.5, 71.8], [164.4, 55.5], [160.7, 48.6], [174.0, 66.4], [163.8, 67.3]]
220 |
221 | }, {
222 | name: 'Male',
223 | color: 'rgba(119, 152, 191, .5)',
224 | data: [[174.0, 65.6], [175.3, 71.8], [193.5, 80.7], [186.5, 72.6], [187.2, 78.8],
225 | [181.5, 74.8], [184.0, 86.4], [184.5, 78.4], [175.0, 62.0], [184.0, 81.6],
226 | [180.0, 76.6], [177.8, 83.6], [192.0, 90.0], [176.0, 74.6], [174.0, 71.0],
227 | [184.0, 79.6], [192.7, 93.8], [171.5, 70.0], [173.0, 72.4], [176.0, 85.9],
228 | [176.0, 78.8], [180.5, 77.8], [172.7, 66.2], [176.0, 86.4], [173.5, 81.8],
229 | [178.0, 89.6], [180.3, 82.8], [180.3, 76.4], [164.5, 63.2], [173.0, 60.9],
230 | [183.5, 74.8], [175.5, 70.0], [188.0, 72.4], [189.2, 84.1], [172.8, 69.1],
231 | [170.0, 59.5], [182.0, 67.2], [170.0, 61.3], [177.8, 68.6], [184.2, 80.1],
232 | [186.7, 87.8], [171.4, 84.7], [172.7, 73.4], [175.3, 72.1], [180.3, 82.6],
233 | [182.9, 88.7], [188.0, 84.1], [177.2, 94.1], [172.1, 74.9], [167.0, 59.1],
234 | [169.5, 75.6], [174.0, 86.2], [172.7, 75.3], [182.2, 87.1], [164.1, 55.2],
235 | [163.0, 57.0], [171.5, 61.4], [184.2, 76.8], [174.0, 86.8], [174.0, 72.2],
236 | [177.0, 71.6], [186.0, 84.8], [167.0, 68.2], [171.8, 66.1], [182.0, 72.0],
237 | [167.0, 64.6], [177.8, 74.8], [164.5, 70.0], [192.0, 101.6], [175.5, 63.2],
238 | [171.2, 79.1], [181.6, 78.9], [167.4, 67.7], [181.1, 66.0], [177.0, 68.2],
239 | [174.5, 63.9], [177.5, 72.0], [170.5, 56.8], [182.4, 74.5], [197.1, 90.9],
240 | [180.1, 93.0], [175.5, 80.9], [180.6, 72.7], [184.4, 68.0], [175.5, 70.9],
241 | [180.6, 72.5], [177.0, 72.5], [177.1, 83.4], [181.6, 75.5], [176.5, 73.0],
242 | [175.0, 70.2], [174.0, 73.4], [165.1, 70.5], [177.0, 68.9], [192.0, 102.3],
243 | [176.5, 68.4], [169.4, 65.9], [182.1, 75.7], [179.8, 84.5], [175.3, 87.7],
244 | [184.9, 86.4], [177.3, 73.2], [167.4, 53.9], [178.1, 72.0], [168.9, 55.5],
245 | [157.2, 58.4], [180.3, 83.2], [170.2, 72.7], [177.8, 64.1], [172.7, 72.3],
246 | [165.1, 65.0], [186.7, 86.4], [165.1, 65.0], [174.0, 88.6], [175.3, 84.1],
247 | [185.4, 66.8], [177.8, 75.5], [180.3, 93.2], [180.3, 82.7], [177.8, 58.0],
248 | [177.8, 79.5], [177.8, 78.6], [177.8, 71.8], [177.8, 116.4], [163.8, 72.2],
249 | [188.0, 83.6], [198.1, 85.5], [175.3, 90.9], [166.4, 85.9], [190.5, 89.1],
250 | [166.4, 75.0], [177.8, 77.7], [179.7, 86.4], [172.7, 90.9], [190.5, 73.6],
251 | [185.4, 76.4], [168.9, 69.1], [167.6, 84.5], [175.3, 64.5], [170.2, 69.1],
252 | [190.5, 108.6], [177.8, 86.4], [190.5, 80.9], [177.8, 87.7], [184.2, 94.5],
253 | [176.5, 80.2], [177.8, 72.0], [180.3, 71.4], [171.4, 72.7], [172.7, 84.1],
254 | [172.7, 76.8], [177.8, 63.6], [177.8, 80.9], [182.9, 80.9], [170.2, 85.5],
255 | [167.6, 68.6], [175.3, 67.7], [165.1, 66.4], [185.4, 102.3], [181.6, 70.5],
256 | [172.7, 95.9], [190.5, 84.1], [179.1, 87.3], [175.3, 71.8], [170.2, 65.9],
257 | [193.0, 95.9], [171.4, 91.4], [177.8, 81.8], [177.8, 96.8], [167.6, 69.1],
258 | [167.6, 82.7], [180.3, 75.5], [182.9, 79.5], [176.5, 73.6], [186.7, 91.8],
259 | [188.0, 84.1], [188.0, 85.9], [177.8, 81.8], [174.0, 82.5], [177.8, 80.5],
260 | [171.4, 70.0], [185.4, 81.8], [185.4, 84.1], [188.0, 90.5], [188.0, 91.4],
261 | [182.9, 89.1], [176.5, 85.0], [175.3, 69.1], [175.3, 73.6], [188.0, 80.5],
262 | [188.0, 82.7], [175.3, 86.4], [170.5, 67.7], [179.1, 92.7], [177.8, 93.6],
263 | [175.3, 70.9], [182.9, 75.0], [170.8, 93.2], [188.0, 93.2], [180.3, 77.7],
264 | [177.8, 61.4], [185.4, 94.1], [168.9, 75.0], [185.4, 83.6], [180.3, 85.5],
265 | [174.0, 73.9], [167.6, 66.8], [182.9, 87.3], [160.0, 72.3], [180.3, 88.6],
266 | [167.6, 75.5], [186.7, 101.4], [175.3, 91.1], [175.3, 67.3], [175.9, 77.7],
267 | [175.3, 81.8], [179.1, 75.5], [181.6, 84.5], [177.8, 76.6], [182.9, 85.0],
268 | [177.8, 102.5], [184.2, 77.3], [179.1, 71.8], [176.5, 87.9], [188.0, 94.3],
269 | [174.0, 70.9], [167.6, 64.5], [170.2, 77.3], [167.6, 72.3], [188.0, 87.3],
270 | [174.0, 80.0], [176.5, 82.3], [180.3, 73.6], [167.6, 74.1], [188.0, 85.9],
271 | [180.3, 73.2], [167.6, 76.3], [183.0, 65.9], [183.0, 90.9], [179.1, 89.1],
272 | [170.2, 62.3], [177.8, 82.7], [179.1, 79.1], [190.5, 98.2], [177.8, 84.1],
273 | [180.3, 83.2], [180.3, 83.2]]
274 | }]
275 | });
276 | });
277 | ```
278 |
279 | If we replace our `chart.js` file with the above javascript code, our page will have the following scatter plot:
280 |
281 | 
282 |
283 | Basically, we are charting female and male height and weight distributions. Let's mess around with our data and see how our scatter plot could look. We'll delete either female or male, rename the remaining one to something else, and change the height and weight to reflect the scatter plot we generated as an SVG.
284 |
285 | ```javascript
286 | $(function () {
287 | $('#container').highcharts({
288 | chart: {
289 | type: 'scatter',
290 | zoomType: 'xy'
291 | },
292 | title: {
293 | text: 'Comparing Random Data of Two Users'
294 | },
295 | subtitle: {
296 | text: 'Source: Ruby Magic'
297 | },
298 | xAxis: {
299 | title: {
300 | enabled: true,
301 | text: 'User1'
302 | },
303 | startOnTick: false,
304 | endOnTick: true,
305 | showLastLabel: true
306 | },
307 | yAxis: {
308 | title: {
309 | text: 'User2'
310 | },
311 | startOnTick: false
312 | },
313 | legend: {
314 | layout: 'vertical',
315 | align: 'left',
316 | verticalAlign: 'top',
317 | x: 100,
318 | y: 70,
319 | floating: true,
320 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF',
321 | borderWidth: 1
322 | },
323 | plotOptions: {
324 | scatter: {
325 | marker: {
326 | radius: 5,
327 | states: {
328 | hover: {
329 | enabled: true,
330 | lineColor: 'rgb(100,100,100)'
331 | }
332 | }
333 | },
334 | states: {
335 | hover: {
336 | marker: {
337 | enabled: false
338 | }
339 | }
340 | },
341 | tooltip: {
342 | headerFormat: '{series.name} ',
343 | pointFormat: 'user1: {point.x}, user2: {point.y}'
344 | }
345 | }
346 | },
347 | series: [{
348 | name: 'Random Data',
349 | color: 'rgba(223, 83, 83, .5)',
350 | data: [[9, 87], [32, 99], [74, 89], [43, 60], [17, 86], [47, 20], [28, 75],
351 | [86, 57], [66, 95], [86, 33], [0, 1], [78, 80], [66, 46], [80, 62],
352 | [45, 73], [50, 18], [74, 10], [74, 21], [37, 36], [92, 47], [39, 5],
353 | [53, 30], [10, 40], [12, 77], [60, 78]]
354 | }]
355 | });
356 | });
357 | ```
358 |
359 | Here, we changed axis variable names and are pretending to compare data based on two users. We're using the same numbers that were generated randomly as our SVG example and updating the `series` data with it. Seems a little cleaner than before. Let's look at the output:
360 |
361 | 
362 |
363 | The graph also looks much nicer than our SVG one! As you can tell, highcharts gives us some nice options especially for pre-built libraries of charts. Take a look at the Highcharts [demo](http://www.highcharts.com/demo) page to see other examples you can make with the library.
364 |
365 | But we still have a pretty important issue to deal with. We can't be manually entering in this data over and over for each new chart we want to build. We want our chart to update dynamically if our data changes. Imagine if we had users `user3` and `user4` with different scatter plot data. We would want our chart to handle that instead of having to manually change labels and data. We'll look at how we might do that in the next section.
366 |
367 | Making a Rails API: [Part 3](03-rails-api.md)
368 |
--------------------------------------------------------------------------------
/03-rails-api.md:
--------------------------------------------------------------------------------
1 | #Interlude: Rails API Generation
2 |
3 | ### The Rails API Roadmap
4 |
5 | Creating a Rails API is very similar to creating a Rails app. We will be following a very similar roadmap, but the data delivered from our controllers will be JSON data.
6 |
7 | Here's a roadmap of how we will take the above data and deliver it in JSON format:
8 |
9 | ```
10 | 1. Install the Rails API gem
11 | 2. Generate a new Rails API app and update our Gemfile.
12 | 3. Create our backend database and write associated migrations.
13 | 4. Update the `routes.rb` file to contain routes we care about.
14 | 5. Write a seed file to populate our database.
15 | 6. See what our data looks like when we hit our API.
16 | 7. Implement ActiveModel Serializers to generate custom JSON data
17 | ```
18 |
19 | Sounds complicated? It's not that bad. Let's get started.
20 |
21 | ####Install the Rails API gem
22 |
23 | ```
24 | gem install rails-api
25 | ```
26 |
27 | The gem has some great [documentation](https://github.com/rails-api/rails-api) that you should take a look at.
28 |
29 | ####Generate a new Rails API app and update our Gemfile.
30 |
31 | ```
32 | rails-api new beer-recommender -T --database=postgresql
33 | ```
34 |
35 | After generating a `beer-recommender` directory, `cd` into it. Let's update our Gemfile so we have everything we need:
36 |
37 | ```ruby
38 |
39 | source 'https://rubygems.org'
40 |
41 | gem 'rails', '4.2.1'
42 |
43 | gem 'rails-api'
44 |
45 | gem 'spring', :group => :development
46 |
47 | gem 'pg'
48 |
49 | gem 'active_model_serializers', '0.8.3'
50 |
51 | group :test do
52 | gem 'shoulda-matchers', require: false
53 | end
54 |
55 | group :development, :test do
56 | gem 'factory_girl_rails'
57 | gem 'faker'
58 | gem 'pry-rails'
59 | gem 'rspec-rails', '~> 3.0.0'
60 | end
61 | ```
62 |
63 | After running `bundle`, generate your test files with rspec by running `rails g rspec:install`.
64 |
65 | Update your `spec/rails_helper.rb` file so that the top of the file looks like this (you are basically just adding the `require 'shoulder/matchers'` under `require 'rspec/rails'`):
66 |
67 | ```ruby
68 | ENV["RAILS_ENV"] ||= 'test'
69 | require 'spec_helper'
70 | require File.expand_path("../../config/environment", __FILE__)
71 | require 'rspec/rails'
72 | require 'shoulda/matchers'
73 | ```
74 |
75 | ####Create our backend database and write associated migrations
76 |
77 | Running `rake db:create` should generate the database from the command line. Once we have a database, we should generate a beer model and associated migration just to see what the data can look like.
78 |
79 | ```ruby
80 | class Beer < ActiveRecord::Base
81 | belongs_to :style
82 |
83 | validates :name, presence: true, uniqueness: true
84 | validates :style, presence: true
85 | end
86 | ```
87 |
88 | Yes, it looks pretty barebones, but let's just go for the basics. We'll have to update the above model later on anyways. Let's add our `Style` table as well:
89 |
90 | ```ruby
91 | class Style < ActiveRecord::Base
92 | has_many :beers
93 |
94 | validates :name, presence: true, uniqueness: true
95 | end
96 | ```
97 |
98 | Now we have to write migrations for our `Beer` and `Style` tables:
99 |
100 | `rails g migration create_beers` will generate a migration file in the `db/migrate` folder.
101 |
102 | ```ruby
103 | class CreateBeers < ActiveRecord::Migration
104 | def change
105 | create_table :beers do |t|
106 | t.string :name, null: false
107 | t.integer :style_id, null: false
108 |
109 | t.timestamps
110 | end
111 |
112 | add_index :beers, :name, unique: true
113 | end
114 | end
115 | ```
116 |
117 | `rails g migration create_styles` will generate a migration file in the `db/migrate` folder.
118 |
119 | ```ruby
120 | class CreateStyles < ActiveRecord::Migration
121 | def change
122 | create_table :styles do |t|
123 | t.string :name, null: false
124 |
125 | t.timestamps
126 | end
127 |
128 | add_index :styles, :name, unique: true
129 | end
130 | end
131 | ```
132 |
133 | Ok, make sure everything looks normal in the schema file and now let's write a small seeder to seed our database:
134 |
135 | ```ruby
136 | styles = ["IPA", "Stout", "Lager", "Wheat Beer"]
137 |
138 | styles.each do |style|
139 | Style.create!(name: style)
140 | end
141 |
142 | style = Style.find_by(name: "IPA")
143 | Beer.create!(name: "Heady Topper", style_id: style.id)
144 | Beer.create!(name: "Pretty Things IPA", style_id: style.id)
145 |
146 | style = Style.find_by(name: "Stout")
147 | Beer.create!(name: "Guinness", style_id: style.id)
148 | Beer.create!(name: "Chocolate Stout", style_id: style.id)
149 |
150 | style = Style.find_by(name: "Lager")
151 | Beer.create!(name: "Brooklyn Lager", style_id: style.id)
152 | Beer.create!(name: "Yuengling", style_id: style.id)
153 |
154 | style = Style.find_by(name: "Wheat Beer")
155 | Beer.create!(name: "Hefeweizen", style_id: style.id)
156 | Beer.create!(name: "Dunkelweizen", style_id: style.id)
157 | ```
158 |
159 | We've now seeded our database with some styles and beers. Play around in `rails console` to see look at various associations!
160 |
161 | ####Update the `config/routes.rb` file
162 |
163 | It will take a little bit of thinking to figure out what we want our API to deliver. The simplest thing we can update our `routes.rb` file with is the ability to be able to see our beers index and styles index pages:
164 |
165 | ```ruby
166 | Rails.application.routes.draw do
167 | namespace :api do
168 | namespace :v1 do
169 | resources :beers, only: [:index]
170 | resources :styles, only: [:index]
171 | end
172 | end
173 | end
174 | ```
175 |
176 | Most of that should look familiar, but why are we namespacing our resources? Because we're building an API! Think about end users of an API. They likely will want a nice, clean url to hit. By namespacing to API and v1, the url they hit will look something like `http://localhost:3000/api/v1/beers` (and similarly for styles). Separating concerns of our API and the rest of our app allows us to be more flexible and modular. Additionally, if we change our API around at some point in the future, we can namespace a v2 for our resources as well.
177 |
178 | If we namespace our api like above, we will have to update our controllers to be in `app/controllers/api/v1/`. In this case, our `BeersController` will be located in `app/controllers/api/v1/beers_controller.rb`:
179 |
180 | ```ruby
181 | class Api::V1::BeersController < ApplicationController
182 | def index
183 | @beers = Beer.all
184 | render json: @beers
185 | end
186 | end
187 | ```
188 |
189 | Wait, what's `render json: @beers` mean? It's pretty much what it looks like. We are grabbing all the beers in our database and then displaying them as a json when we go to `localhost:3000/api/v1/beers`. We'll do something similar with styles.
190 |
191 | Let's write our `app/controllers/api/v1/styles_controller.rb` file:
192 |
193 | ```ruby
194 | class Api::V1::StylesController < ApplicationController
195 | def index
196 | @styles = Style.all
197 | render json: @styles
198 | end
199 | end
200 | ```
201 |
202 | Run `rails server` and visit these pages and see what they look like:
203 |
204 | Beers index page:
205 |
206 | 
207 |
208 | Styles index page:
209 |
210 | 
211 |
212 | Great! Our beers and styles are being delivered as JSON to their respective api/v1 index pages. Append `.json` to the end of each page and see what happens. Keep that in mind for the future!
213 |
214 | ####ActiveModel Serializers
215 |
216 | Say we wanted to deliver the total number of beers by style when we access `/api/v1/styles`. How would we do that? We can take advantage of a tool called [ActiveModel Serializers](https://github.com/rails-api/active_model_serializers), which serializes the data we want to send through our API.
217 |
218 | We can pass a beers count directly into the Styles JSON when our user accesses `/api/v1/styles`. But first, we need to add a `serializers` directory to `app`. So let's make one for `Style` first:
219 |
220 | `touch app/serializers/style_serializer.rb`
221 |
222 | ```ruby
223 | class StyleSerializer < ActiveModel::Serializer
224 | embed :ids
225 |
226 | attributes :id, :name, :beers_count
227 |
228 | def beers_count
229 | object.beers.count
230 | end
231 | end
232 | ```
233 |
234 | Let's take a look at `/app/v1/styles` now!:
235 |
236 | 
237 |
238 | Note that we're only delivering the *attributes* that we have specifically stated in our `Style` serializer: **id**, **name**, and **beers_count**. This is why we don't see the timestamps like we did before. We are being quite specific here and determining which attributes we want to pass on.
239 |
240 | That seems like something we can chart, right? The *x*-axis would be the name of the style and the *y*-axis would be the number of beers of that particular style in the database.
241 |
242 | ActiveModel Serializers seem pretty powerful! What else can we do? Well, we don't have a whole lot of data to play around with, but what if we had more?
243 |
244 | Let's take a look at the data we have.
245 |
246 | **By the way**, access to the extensive reviews from RateBeer which formed a large part of the database of this project could not have been possible without the assistance of Professor Julian McAuley of UCSD, who was was kind enough to share the data with me. More on his research around beer and recommendations can be found here: http://snap.stanford.edu/data/web-RateBeer.html. Note, please contact me directly as you will need access to the RateBeer data which the company has asked not to share publicly. I won't be sharing the entire data set, but snippets of it which you can use to create your own fake data to play with.
247 |
248 | The following is a snippet of the first handful of records from a `csv` file that contains only a few columns we'll care about right now: `name`, `beer_id`, `ABV`, `style`, `taste`, `profile_name` and `text`.
249 |
250 | ```
251 | name,beer_id,brewer_id,ABV,style,appearance,aroma,palate,taste,overall,time,profile_name,text
252 | John Harvards Simcoe IPA,63836,8481,5.4,India Pale Ale (IPA),4/5,6/10,3/5,6/10,13/20,1157587200,hopdog,"On tap at the Springfield, PA location. Poured a deep and cloudy orange (almost a copper) color with a small sized off white head. Aromas or oranges and all around citric. Tastes of oranges, light caramel and a very light grapefruit finish. I too would not believe the 80+ IBUs - I found this one to have a very light bitterness with a medium sweetness to it. Light lacing left on the glass."
253 | John Harvards Simcoe IPA,63836,8481,5.4,India Pale Ale (IPA),4/5,6/10,4/5,7/10,13/20,1157241600,TomDecapolis,"On tap at the John Harvards in Springfield PA. Pours a ruby red amber with a medium off whie creamy head that left light lacing. Aroma of orange and various other citrus. A little light for what I was expecting from this beers aroma...expecting more from the Simcoe. Flavor of pine, orange, grapefruit and some malt balance. Very light bitterness for the 80+ IBUs they said this one had."
254 | John Harvards Cristal Pilsner,71716,8481,5,Bohemian Pilsener,4/5,5/10,3/5,6/10,14/20,958694400,PhillyBeer2112,"UPDATED: FEB 19, 2003 Springfield, PA. I've never had the Budvar Cristal but this is exactly what I imagined it to be. A clean and refreshing, hoppy beer, med bodied with plenty of flavor. This beer's only downfall is an unpleasant bitterness in the aftertaste."
255 | John Harvards Fancy Lawnmower Beer,64125,8481,5.4,Kölsch,2/5,4/10,2/5,4/10,8/20,1157587200,TomDecapolis,"On tap the Springfield PA location billed as the ""Fancy Lawnmower Light"". Pours a translucent clear yellow with a small bubbly white head. Aroma was lightly sweet and malty, really no hop presence. Flavor was light, grainy, grassy and malty. Just really light in flavor and aroma overall. Watery."
256 | John Harvards Fancy Lawnmower Beer,64125,8481,5.4,Kölsch,2/5,4/10,2/5,4/10,8/20,1157587200,hopdog,"On tap at the Springfield, PA location. Poured a lighter golden color with a very small, if any head. Aromas and tastes of grain, very lightly fruity with a light grassy finish. Lively yet thin and watery body. Oh yeah, the person seating me told me this was a new one and was a Pale Ale even though the menu he gave me listed it as a lighter beer brewed in the Kolsh style."
257 | John Harvards Vanilla Black Velvet Stout,31544,8481,-,Sweet Stout,5/5,8/10,4/5,7/10,16/20,1077753600,egajdzis,"Springfield, PA location... Poured an opaque black color with a creamy tan head and nice lacing. Strong vanilla and roasted malt aroma. Creamy taste of coffee, chocolate and vanilla. The bartender told me this was an imperial stout at about 8%. She didn't convince me, there was no alcohol to be found, and it was sweet as hell! But still good."
258 | John Harvards American Brown Ale,71714,8481,-,Brown Ale,4/5,5/10,3/5,6/10,12/20,1176076800,hopdog,"On tap at the Springfield, PA location. Listed on the beer menu as ""James Brown Ale"". Had the regular and cask version. Poured a deep brown color with an averaged sized off white head (cask had a huge head). Ill stop on the cask version here as I found it to smell and taste like buttered popcorn. The regular had aromas of nuts, light chocolate, and roast. Taste of chocolate, nuts, very light roast and caramel. Tasted on 9/7/2006 and moved over as part of the John Harvard clean up."
259 | John Harvards Grand Cru,71719,8481,7,Belgian Ale,2/5,6/10,3/5,7/10,14/20,1107302400,JFGrind,"Sampled @ the Springfield, PA location. Candi Sugar dominates this Belgian Ale. Beer was on the flat side but had a nice crimson color. Enjoyable Belgian Ale, I did not expect John Harvards to have it in its line-up."
260 | John Harvards Grand Cru,71719,8481,7,Belgian Ale,4/5,8/10,3/5,7/10,16/20,1102896000,egajdzis,"Springfield... Poured a hazy copper color with a medium sized, off white head that left spotty lacing on the glass. Aroma of yeast, dried fruits, clove, banana, and cherries, with light roastiness. Aroma was very dubbelish. Herbal taste of dark fruits, yeast and alcohol was barely noticed. Slick mouthfeel. Could have been more flavorful."
261 | ```
262 |
263 | Normally it's not the best idea to drop databases and delete migrations, but since we don't really have any mission critical data, I'm going to be doing that right here. My seed data is comprehensive, so it's worth taking a moment and cleaning up what I need exactly. What tables will I need and what will their columns and associations be?
264 |
265 | ```
266 | (1) Beer: id, name, style_id, abv
267 | has_many reviews, belongs_to style
268 |
269 | (2) Style: id, name
270 | has_many beers
271 |
272 | (3) User: id, profile_name
273 | has_many reviews, has_many beers through reviews
274 |
275 | (4) Review: id, beer_id, user_id, taste, text
276 | belongs_to beer, belongs_to user
277 | ```
278 |
279 | Those are all the models we'll need:
280 |
281 | ```ruby
282 | class Beer < ActiveRecord::Base
283 | belongs_to :style
284 | has_many :reviews
285 |
286 | validates :name, presence: true, uniqueness: true
287 | validates :style, presence: true
288 | end
289 | ```
290 |
291 | ```ruby
292 | class Style < ActiveRecord::Base
293 | has_many :beers
294 |
295 | validates :name, presence: true, uniqueness: true
296 | end
297 | ```
298 |
299 | ```ruby
300 | class User < ActiveRecord::Base
301 | has_many :reviews
302 | has_many :beers, through: :reviews
303 |
304 | validates :profile_name, presence: true, uniqueness: true
305 | end
306 | ```
307 |
308 | ```ruby
309 | class Review < ActiveRecord::Base
310 | belongs_to :beer
311 | belongs_to :user
312 |
313 | validates :beer, presence: true
314 | validates :taste, presence: true
315 | end
316 | ```
317 |
318 | And these are the migrations we'll need:
319 |
320 | ```ruby
321 | class CreateBeers < ActiveRecord::Migration
322 | def change
323 | create_table :beers do |t|
324 | t.string :name, null: false
325 | t.integer :style_id, null: false
326 | t.float :abv
327 |
328 | t.timestamps
329 | end
330 |
331 | add_index :beers, :name, unique: true
332 | end
333 | end
334 | ```
335 |
336 | ```ruby
337 | class CreateStyles < ActiveRecord::Migration
338 | def change
339 | create_table :styles do |t|
340 | t.string :name, null: false
341 |
342 | t.timestamps
343 | end
344 |
345 | add_index :styles, :name, unique: true
346 | end
347 | end
348 | ```
349 |
350 | ```ruby
351 | class CreateUsers < ActiveRecord::Migration
352 | def change
353 | create_table :users do |t|
354 | t.string :profile_name, null: false
355 |
356 | t.timestamps
357 | end
358 |
359 | add_index :users, :profile_name, unique: true
360 | end
361 | end
362 | ```
363 |
364 | ```ruby
365 | class CreateReviews < ActiveRecord::Migration
366 | def change
367 | create_table :reviews do |t|
368 | t.integer :beer_id, null: false
369 | t.integer :user_id, null: false
370 | t.float :taste, null: false
371 | t.text :text
372 |
373 | t.timestamps
374 | end
375 |
376 | add_index :reviews, :beer_id
377 | add_index :reviews, :user_id
378 | end
379 | end
380 | ```
381 |
382 | And here is the code to seed (`db/seeds.rb`) our database from a larger version of the CSV snippet provided above:
383 |
384 | ```ruby
385 | require 'csv'
386 |
387 | file = File.read('db/data/Ratebeer.csv')
388 | csv = CSV.parse(file, :headers => true, :header_converters => :symbol)
389 |
390 | count = 1
391 | csv.each do |row|
392 | puts "Creating style with name: #{row[:style]}"
393 | style = Style.find_or_create_by!(name: row[:style])
394 |
395 | puts "Creating user with profile_name: #{row[:profile_name]}"
396 | if !User.find_by(profile_name: row[:profile_name])
397 | user = User.create!(profile_name: row[:profile_name])
398 | else
399 | user = User.find_by(profile_name: row[:profile_name])
400 | end
401 |
402 | puts "Creating beer with name: #{row[:name]}"
403 | beer = Beer.find_or_initialize_by(row.to_hash.slice(:name, :abv))
404 | beer.style = style
405 | beer.save!
406 |
407 | puts "Creating review"
408 | review = user.reviews.find_or_initialize_by(beer: beer)
409 | review_attrs = row.to_hash.slice(:taste, :text)
410 | review.update_attributes!(review_attrs)
411 |
412 | puts "Completed Row #:#{count}"
413 | count += 1
414 | end
415 | ```
416 |
417 | After letting that seeder run, let's take a look at `localhost:3000/api/v1/styles` and see how it looks:
418 |
419 | 
420 |
421 | Nice! Now we have a lot of data we can look at. What else can we do?
422 |
423 | Maybe we want to see how many reviews a beer has on the beers index page? We'll have to add a Beer Serializer in `beer_serializer.rb`:
424 |
425 | ```ruby
426 | class BeerSerializer < ActiveModel::Serializer
427 | embed :ids
428 |
429 | attributes :id, :name, :style_id, :abv, :reviews_count
430 |
431 | def reviews_count
432 | object.reviews.count
433 | end
434 | end
435 | ```
436 |
437 | Let's take a look at `api/v1/beers` now:
438 |
439 | 
440 |
441 | Looks good, but it took forever to load! Probably because I was calculating `reviews.count` for each beer. We can implement a [counter_cache](http://guides.rubyonrails.org/association_basics.html#detailed-association-reference) to avoid that, so let's just do that now:
442 |
443 | ```ruby
444 | #app/models/review.rb
445 |
446 | class Review < ActiveRecord::Base
447 | belongs_to :beer, counter_cache: true
448 | belongs_to :user
449 |
450 | validates :beer, presence: true
451 | validates :taste, presence: true
452 | end
453 | ```
454 |
455 | We also need to add a `reviews_count` column to `Beer` table as a migration:
456 |
457 | ```ruby
458 | #db/migrate/xxxxxxxx_add_reviews_count_to_beers.rb
459 |
460 | class AddReviewsCountToBeers < ActiveRecord::Migration
461 | def change
462 | add_column :beers, :reviews_count, :integer
463 | end
464 | end
465 | ```
466 |
467 | Great! All we need to do now is run the following in `rails console` to update the counters:
468 |
469 | ```ruby
470 | Beer.find_each { |beer| Beer.reset_counters(beer.id, :reviews)}
471 | ```
472 |
473 | Try loading the page again. See how much faster it took?
474 |
475 | Let's update our `Beers` controller to update to return a JSON of beers sorted by `reviews_count`:
476 |
477 | ```ruby
478 | class Api::V1::BeersController < ApplicationController
479 | def index
480 | @beers = Beer.order("reviews_count desc")
481 | render json: @beers
482 | end
483 | end
484 | ```
485 |
486 | And the output:
487 |
488 | 
489 |
490 | Chimay and Lagunitas seem pretty popular to review!
491 |
492 | See how valuable JSON data is in a format that we can do something with? Now let's actually chart some data!
493 |
494 | Highcharts with a Rails API: [Part 4](04-rails-api-highcharts.md)
495 |
--------------------------------------------------------------------------------
/04-rails-api-highcharts.md:
--------------------------------------------------------------------------------
1 | #Wiring up Highcharts with a Rails API
2 |
3 | In the [last section](03-rails-api.md), we made a simple API that delivered beer review related data in JSON format. Now let's figure out how to take that JSON format and visualize it.
4 |
5 | The primary question we'll try to answer here is this: How do we pass JSON data to Highcharts?
6 |
7 | Well, let's break this problem down. First we'll have to hit our API to grab the data we care about. Since this happens on the client side, we'll use Javascript to do that. Here's the pseudocoded version of what we're going to have to do
8 |
9 | ```
10 | (1) Write Javascript code to hit Rails API
11 | (2) Grab Rails API JSON response and parse at how we need it
12 | (3) Pass parsed data to Highcharts chart
13 | ```
14 |
15 | One thing we need to keep in mind is that we can't just hit `http://localhost:3000/api/v1/beers` and store the response as a variable in Javascript. For security reasons (which you can read more about [here](http://en.wikipedia.org/wiki/Same-origin_policy)), we'll have to hit another url:
16 |
17 | `http://localhost:3000/api/v1/beers.jsonp?callback=?`
18 |
19 | Javascript will generate a callback which will allow us to store our JSON in JSONP format. Additionally, we'll have to update our `BeersController` to allow for this.
20 |
21 | Update Beers index controller:
22 |
23 | ```ruby
24 | class Api::V1::BeersController < ApplicationController
25 | def index
26 | @beers = Beer.order("reviews_count desc")
27 | render json: @beers, callback: params['callback']
28 | end
29 | end
30 | ```
31 |
32 | Let's just try to get `Oooh, it worked!` to pop up on the screen if our `.getJSON` is successful:
33 |
34 | ```javascript
35 | var url = "http://localhost:3000/api/v1/beers.jsonp?callback=?";
36 |
37 | $.getJSON(url, function (json) {
38 | alert("Oooh, it worked!");
39 | });
40 | ```
41 |
42 | 
43 |
44 | Perfect! Now let's see the name of the first beer!
45 |
46 | ```javascript
47 | var url = "http://localhost:3000/api/v1/beers.jsonp?callback=?";
48 |
49 | $.getJSON(url, function (json) {
50 | alert(json.beers[0].name);
51 | });
52 | ```
53 |
54 | 
55 |
56 | Finally. Looks like we're making some progress and can start building our charts. What's next?
57 |
58 | Let's figure out how to print the top 10 beers by number of reviews (or `reviews_count`). Let's grab the [Column with Rotated Labels](http://www.highcharts.com/demo/column-rotated-labels) chart on the Highcharts website and replicate it for our data.
59 |
60 | ```javascript
61 | $(function () {
62 | $('#container').highcharts({
63 | chart: {
64 | type: 'column'
65 | },
66 | title: {
67 | text: 'World\'s largest cities per 2014'
68 | },
69 | subtitle: {
70 | text: 'Source: Wikipedia'
71 | },
72 | xAxis: {
73 | type: 'category',
74 | labels: {
75 | rotation: -45,
76 | style: {
77 | fontSize: '13px',
78 | fontFamily: 'Verdana, sans-serif'
79 | }
80 | }
81 | },
82 | yAxis: {
83 | min: 0,
84 | title: {
85 | text: 'Population (millions)'
86 | }
87 | },
88 | legend: {
89 | enabled: false
90 | },
91 | tooltip: {
92 | pointFormat: 'Population in 2008: {point.y:.1f} millions'
93 | },
94 | series: [{
95 | name: 'Population',
96 | data: [
97 | ['Shanghai', 23.7],
98 | ['Lagos', 16.1],
99 | ['Instanbul', 14.2],
100 | ['Karachi', 14.0],
101 | ['Mumbai', 12.5],
102 | ['Moscow', 12.1],
103 | ['São Paulo', 11.8],
104 | ['Beijing', 11.7],
105 | ['Guangzhou', 11.1],
106 | ['Delhi', 11.1],
107 | ['Shenzhen', 10.5],
108 | ['Seoul', 10.4],
109 | ['Jakarta', 10.0],
110 | ['Kinshasa', 9.3],
111 | ['Tianjin', 9.3],
112 | ['Tokyo', 9.0],
113 | ['Cairo', 8.9],
114 | ['Dhaka', 8.9],
115 | ['Mexico City', 8.9],
116 | ['Lima', 8.9]
117 | ],
118 | dataLabels: {
119 | enabled: true,
120 | rotation: -90,
121 | color: '#FFFFFF',
122 | align: 'right',
123 | format: '{point.y:.1f}', // one decimal
124 | y: 10, // 10 pixels down from the top
125 | style: {
126 | fontSize: '13px',
127 | fontFamily: 'Verdana, sans-serif'
128 | }
129 | }
130 | }]
131 | });
132 | });
133 | ```
134 |
135 | The above Javascript generates the following chart:
136 |
137 | 
138 |
139 | Let's start by just doing the easy stuff like changing the labels. Then we can wrap that code in our API request and adjust the data accordingly:
140 |
141 | ```javascript
142 | $(function () {
143 | $('#container').highcharts({
144 | chart: {
145 | type: 'column'
146 | },
147 | title: {
148 | text: 'Popular Beers by Number of Reviews'
149 | },
150 | subtitle: {
151 | text: 'Source: Ratebeer'
152 | },
153 | xAxis: {
154 | type: 'category',
155 | labels: {
156 | rotation: -45,
157 | style: {
158 | fontSize: '13px',
159 | fontFamily: 'Verdana, sans-serif'
160 | }
161 | }
162 | },
163 | yAxis: {
164 | min: 0,
165 | title: {
166 | text: 'Number of Reviews'
167 | }
168 | },
169 | legend: {
170 | enabled: false
171 | },
172 | tooltip: {
173 | pointFormat: 'Number of Reviews: {point.y:.1f}'
174 | },
175 | series: [{
176 | name: 'Population',
177 | data: [
178 | ['Shanghai', 23.7],
179 | ['Lagos', 16.1],
180 | ['Instanbul', 14.2],
181 | ['Karachi', 14.0],
182 | ['Mumbai', 12.5],
183 | ['Moscow', 12.1],
184 | ['São Paulo', 11.8],
185 | ['Beijing', 11.7],
186 | ['Guangzhou', 11.1],
187 | ['Delhi', 11.1],
188 | ],
189 | dataLabels: {
190 | enabled: true,
191 | rotation: -90,
192 | color: '#FFFFFF',
193 | align: 'right',
194 | format: '{point.y:.1f}', // one decimal
195 | y: 10, // 10 pixels down from the top
196 | style: {
197 | fontSize: '13px',
198 | fontFamily: 'Verdana, sans-serif'
199 | }
200 | }
201 | }]
202 | });
203 | });
204 | ```
205 |
206 | 
207 |
208 | We just renamed some of the labels (*y* axis label, for example) and removed some of the cities from the list. Now we need to replace the city data with beer data. We can wrap our above code in our API request and pass the data we care about to `data`:
209 |
210 | ```javascript
211 | var url = "http://localhost:3000/api/v1/beers.jsonp?callback=?";
212 |
213 | $.getJSON(url, function (json) {
214 | $(function () {
215 | $('#container').highcharts({
216 | .
217 | .
218 | .
219 | });
220 | ```
221 |
222 | We need to create an array of arrays in the format that's required by Highcharts. Our data will look something like this:
223 |
224 | ```
225 | [["Beer1", 3000], ["Beer2", 2000], ["Beer3", 1500], etc.]
226 | ```
227 |
228 | To get the data to look like that, we'll do a bit of data manipulation in Javascript. We will loop through `json` to generate an array of arrays:
229 |
230 | ```javascript
231 | var url = "http://localhost:3000/api/v1/beers.jsonp?callback=?";
232 |
233 | $.getJSON(url, function (json) {
234 | var beersAndReviews = new Array;
235 | for (i = 0; i < 10; i++) {
236 | beersAndReviews.push([json.beers[i].name, json.beers[i].reviews_count])
237 | }
238 | $(function () {
239 | $('#container').highcharts({
240 | .
241 | .
242 | .
243 | ```
244 |
245 | Once we're certain that `beersAndReviews` contains the top 10 beers by reviews, we can update the `series` part of our code:
246 |
247 | ```javascript
248 | series: [{
249 | name: 'Beer',
250 | data: beersAndReviews,
251 | dataLabels: {
252 | enabled: true,
253 | rotation: -90,
254 | color: '#FFFFFF',
255 | align: 'right',
256 | format: '{point.y}',
257 | y: 10,
258 | style: {
259 | fontSize: '12px',
260 | fontFamily: 'Verdana, sans-serif'
261 | }
262 | }
263 | }]
264 | ```
265 |
266 | After our update, let's load up our chart:
267 |
268 | 
269 |
270 | That wasn't too bad! It just took a little bit of figuring out how to make the API call. The actual data manipulation was fairly straightforward. Now we have a nice visual of our data. Just by glancing at our chart, we can see that Chimay Blue has 30% more reviews than Chimay Rouge and the next closest brewer is Lagunitas. Pretty interesting. What else can we do?
271 |
272 | Well, we started this whole journey by looking at scatter plots, so why don't we finish up with one? We'll plot some user beer preferences. Our API doesn't provide us the ability to see let alone manipulate, user data just yet, so we have to write that functionality in.
273 |
274 | What do we want our API url to look like? There are probably a number of ways to do this, but the following is a straightforward one that comes to mind:
275 |
276 | `http://localhost:3000/api/v1/ratings/1?compare=2`
277 |
278 | This way we can have access to both `id`s of the users we want to compare, 1 and 2, in this case. Here's another example:
279 |
280 | `http://localhost:3000/api/v1/ratings/1200?compare=18`
281 |
282 | That API call will return ideally return a comparison of users with `id`s of 1200 and 18.
283 |
284 | First, let's update our `config/routes.rb` file to allow for this. Note that I'm going to limit the `users` route to the show action:
285 |
286 | ```ruby
287 | Rails.application.routes.draw do
288 | namespace :api do
289 | namespace :v1 do
290 | resources :beers, only: [:index]
291 | resources :styles, only: [:index]
292 | resources :ratings, only: [:show]
293 | end
294 | end
295 | end
296 | ```
297 |
298 | Now we need to add a `RatingsController` in `app/controllers/api/v1/ratings_controller.rb`:
299 |
300 | ```ruby
301 | class Api::V1::RatingsController < ApplicationController
302 | def show
303 | end
304 | end
305 | ```
306 |
307 | Let's leave it mostly empty before we figure out what to to next. We'll have to do a few things now. Grab the beers that both have reviewed in common and return a json of those. We'll have to grab one user's data and then grab another's and see which beers they have in common that they have reviewed. Since a user `has_many beers through reviews`, we can simply return the intersection of the beers reviewed with another user.
308 |
309 | ```
310 | (1) Take two users
311 | (2) Get a list of beers they have reviewed in common
312 | (3) Get the ratings of each of those beers by each user and return those
313 | ```
314 |
315 | Great. Now let's update our RatingsController to do just that. We'll probably want to refactor a lot of this later on, but for now, let's get it to work!:
316 |
317 | ```ruby
318 | class Api::V1::RatingsController < ApplicationController
319 | def show
320 | user1 = User.find(params[:id])
321 | user2 = User.find(params[:compare])
322 | beers_in_common = user1.beers & user2.beers
323 | @beers_in_common_with_ratings = Array.new
324 | beers_in_common.each do |beer|
325 | user1_rating = beer.reviews.find_by(user_id: user1.id).taste
326 | user2_rating = beer.reviews.find_by(user_id: user2.id).taste
327 | @beers_in_common_with_ratings << {name: beer.name, user1_rating: user1_rating, user2_rating: user2_rating}
328 | end
329 | render json: @beers_in_common_with_ratings, callback: params['callback']
330 | end
331 | end
332 | ```
333 |
334 | Here's what we're doing. We're grabbing two user objects and then intersection the arrays of beers they have in common (the `&` operator returns the intersection of two arrays). Then we're generating a json friendly data structure called `@beers_in_common_with_ratings` that stores each of the intersected beer's respective user ratings. What does `http://localhost:3000/api/v1/ratings/1?compare=2` look like now?. Let's see!
335 |
336 | 
337 |
338 | Great! Now we have enough data to make a scatter plot, so let's get to work! This is what the Javascript of the scatter plot in Part 2 of this tutorial looked like:
339 |
340 | ```javascript
341 | $(function () {
342 | $('#container').highcharts({
343 | chart: {
344 | type: 'scatter',
345 | zoomType: 'xy'
346 | },
347 | title: {
348 | text: 'Comparing Random Data of Two Users'
349 | },
350 | subtitle: {
351 | text: 'Source: Ruby Magic'
352 | },
353 | xAxis: {
354 | title: {
355 | enabled: true,
356 | text: 'User1'
357 | },
358 | startOnTick: false,
359 | endOnTick: true,
360 | showLastLabel: true
361 | },
362 | yAxis: {
363 | title: {
364 | text: 'User2'
365 | },
366 | startOnTick: false
367 | },
368 | legend: {
369 | layout: 'vertical',
370 | align: 'left',
371 | verticalAlign: 'top',
372 | x: 100,
373 | y: 70,
374 | floating: true,
375 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF',
376 | borderWidth: 1
377 | },
378 | plotOptions: {
379 | scatter: {
380 | marker: {
381 | radius: 5,
382 | states: {
383 | hover: {
384 | enabled: true,
385 | lineColor: 'rgb(100,100,100)'
386 | }
387 | }
388 | },
389 | states: {
390 | hover: {
391 | marker: {
392 | enabled: false
393 | }
394 | }
395 | },
396 | tooltip: {
397 | headerFormat: '{series.name} ',
398 | pointFormat: 'user1: {point.x}, user2: {point.y}'
399 | }
400 | }
401 | },
402 | series: [{
403 | name: 'Random Data',
404 | color: 'rgba(223, 83, 83, .5)',
405 | data: [[9, 87], [32, 99], [74, 89], [43, 60], [17, 86], [47, 20], [28, 75],
406 | [86, 57], [66, 95], [86, 33], [0, 1], [78, 80], [66, 46], [80, 62],
407 | [45, 73], [50, 18], [74, 10], [74, 21], [37, 36], [92, 47], [39, 5],
408 | [53, 30], [10, 40], [12, 77], [60, 78]]
409 | }]
410 | });
411 | });
412 | ```
413 |
414 | Using the API knowledge we gained earlier, let's start updating this Javascript:
415 |
416 | ```javascript
417 | var url = "http://localhost:3000/api/v1/ratings/1?compare=2.jsonp?callback=?";
418 |
419 | $.getJSON(url, function (json) {
420 | .
421 | .
422 | .
423 | ```
424 |
425 | Because we have a comparison and a callback in our API url, we'll have to do a bit of data manipulation on the `RatingsController` side:
426 |
427 | ```ruby
428 | class Api::V1::RatingsController < ApplicationController
429 | def show
430 | params[:format] = "jsonp"
431 | user1 = User.find(params[:id])
432 | user2 = User.find(params[:compare].split(".").first)
433 | beers_in_common = user1.beers & user2.beers
434 | @beers_in_common_with_ratings = Array.new
435 | beers_in_common.each do |beer|
436 | user1_rating = beer.reviews.find_by(user_id: user1.id).taste
437 | user2_rating = beer.reviews.find_by(user_id: user2.id).taste
438 | @beers_in_common_with_ratings << {name: beer.name, user1_rating: user1_rating, user2_rating: user2_rating}
439 | end
440 | render json: @beers_in_common_with_ratings, callback: params[:compare].split("=").last
441 | end
442 | end
443 | ```
444 |
445 | Now we can manipulate the data we got back:
446 |
447 | ```javascript
448 | var url = "http://localhost:3000/api/v1/ratings/1?compare=2.jsonp?callback=?";
449 |
450 | $.getJSON(url, function (json) {
451 | var ratings = new Array;
452 | var count = json.ratings.length;
453 | for (i = 0; i < count; i++) {
454 | ratings.push([json.ratings[i].user1_rating, json.ratings[i].user2_rating]);
455 | }
456 | .
457 | .
458 | .
459 | series: [{
460 | name: 'Beer Data',
461 | color: 'rgba(223, 83, 83, .5)',
462 | data: ratings
463 | }]
464 | });
465 | });
466 | });
467 | ```
468 |
469 | Let's take a look at this work on the scatter plot!:
470 |
471 | 
472 |
473 | Nice! Right now we're just comparing user with `id` of 1 and user with id of `2` but later on we could make our Javascript more dynamic by allowing us to enter in which users we want to compare. We still have some work to do (like cleaning up our controller to deal with non-existent users and users who don't share beers), but for now, we're in a good place.
474 |
475 | Thanks for going through this tutorial! I hope it sheds some light on the power of Rails APIs and how we can take advantage of Highcharts and Javascript to make some simple but powerful data visualizations.
476 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Introduction
2 |
3 | Tools to generate charts, graphs, plots are quite popular these days, and for good reason. They can help turn millions of numbers into something more tangible and understandable. [Knowledgent](http://knowledgent.com/infographics/data-viz-101/), a data analysis firm, says the following about data visualization:
4 |
5 | ```
6 | A well-crafted data visualization helps uncover trends,
7 | realize insights, explore sources, and tell stories.
8 | ```
9 |
10 | Let's look at an example here. Of the two following (1) or (2), which one *appears* to be more meaningful than the other?:
11 |
12 | (1)
13 | ```ruby
14 | preferences =
15 | {"beer1"=>[24, 25],
16 | "beer2"=>[57, 59],
17 | "beer3"=>[41, 38],
18 | "beer4"=>[46, 48],
19 | "beer5"=>[77, 80],
20 | "beer6"=>[72, 67],
21 | "beer7"=>[14, 11],
22 | "beer8"=>[84, 79],
23 | "beer9"=>[34, 39],
24 | "beer10"=>[20, 19],
25 | "beer11"=>[21, 16],
26 | "beer12"=>[73, 69],
27 | "beer13"=>[35, 40],
28 | "beer14"=>[83, 85],
29 | "beer15"=>[40, 45],
30 | "beer16"=>[10, 11],
31 | "beer17"=>[71, 68],
32 | "beer18"=>[45, 50],
33 | "beer19"=>[56, 54],
34 | "beer20"=>[20, 23]}
35 | ```
36 |
37 | (2)
38 | 
39 |
40 | Each point in the scatter plot is a comparison of two user's beer ratings of a particular beer. The *x* coordinate is the rating of a particular beer for me and the *y* coordinate is the rating for that particular beer for you. Looks like we both like `beer8` (which is probably some really hoppy IPA if you're like me) a lot. Unless you have some impressive mental data visualization tools at your cognitive disposal, (2) provides some meaningful analysis quickly. Without (2), we would be harder pressed to glean something important from (1). Even though (1) and (2) *contain* the same data, the *representation* of that data makes the difference. Imagine if we had 1,000 beers to compare between one another or if we had a database of 1,000,000 users! Which approach would could help us say something meaningful? Let's figure out how we can take advantage of the data visualization tools out there.
41 |
42 | This tutorial will cover the following
43 |
44 | * What tools can I use to visualize data with html?
45 | * How do I make a Rails API to deliver data I care about?
46 | * How do I visualize the data from my Rails API?
47 |
48 | ##Part 1 - Scalable Vector Graphics
49 | This section covers SVG, or *scalable vector graphics*, as an introduction to graphing data with html.
50 |
51 | [Making Graphs Manually with SVG](01-svg.md)
52 |
53 | ##Part 2 - The Highcharts Javascript Charting Library
54 | This section is an introduction to the [Highcharts](http://www.highcharts.com/) Javascript library and how we can use it to make nice looking charts from simple ones like bar charts to more complex ones like scatter plots.
55 |
56 | [Highcharts](02-highcharts.md)
57 |
58 | ##Part 3 - Making a Rails API
59 | This section will explain how to create a simple Rails API with the Rails API gem and ActiveModel Serializers. It reviews the basics of creating models, controllers and migrations but in the context of a namespaced API.
60 |
61 | [Making a Rails API](03-rails-api.md)
62 |
63 | ##Part 4 - Highcharts with a Rails API
64 | This section highlights connecting a Rails API with Highcharts. It covers data manipulation on both the server and client side to generate both bar and scatter plots from our Rails API.
65 |
66 | [Hooking up your Rails API with Highcharts](04-rails-api-highcharts.md)
67 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/js/chart.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $('#container').highcharts({
3 | chart: {
4 | type: 'scatter',
5 | zoomType: 'xy'
6 | },
7 | title: {
8 | text: 'Comparing Random Data of Two Users'
9 | },
10 | subtitle: {
11 | text: 'Source: Ruby Magic'
12 | },
13 | xAxis: {
14 | title: {
15 | enabled: true,
16 | text: 'User1'
17 | },
18 | startOnTick: false,
19 | endOnTick: true,
20 | showLastLabel: true
21 | },
22 | yAxis: {
23 | title: {
24 | text: 'User2'
25 | },
26 | startOnTick: false
27 | },
28 | legend: {
29 | layout: 'vertical',
30 | align: 'left',
31 | verticalAlign: 'top',
32 | x: 100,
33 | y: 70,
34 | floating: true,
35 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF',
36 | borderWidth: 1
37 | },
38 | plotOptions: {
39 | scatter: {
40 | marker: {
41 | radius: 5,
42 | states: {
43 | hover: {
44 | enabled: true,
45 | lineColor: 'rgb(100,100,100)'
46 | }
47 | }
48 | },
49 | states: {
50 | hover: {
51 | marker: {
52 | enabled: false
53 | }
54 | }
55 | },
56 | tooltip: {
57 | headerFormat: '{series.name} ',
58 | pointFormat: 'user1: {point.x}, user2: {point.y}'
59 | }
60 | }
61 | },
62 | series: [{
63 | name: 'Random Data',
64 | color: 'rgba(223, 83, 83, .5)',
65 | data: [[9, 87], [32, 99], [74, 89], [43, 60], [17, 86], [47, 20], [28, 75],
66 | [86, 57], [66, 95], [86, 33], [0, 1], [78, 80], [66, 46], [80, 62],
67 | [45, 73], [50, 18], [74, 10], [74, 21], [37, 36], [92, 47], [39, 5],
68 | [53, 30], [10, 40], [12, 77], [60, 78]]
69 | }]
70 | });
71 | });
72 |
73 |
--------------------------------------------------------------------------------
/js/chart2.js:
--------------------------------------------------------------------------------
1 | var url = "http://localhost:3000/api/v1/beers.jsonp?callback=?";
2 |
3 | $.getJSON(url, function (json) {
4 | var beersAndReviews = new Array;
5 | for (i = 0; i < 10; i++) {
6 | beersAndReviews.push([json.beers[i].name, json.beers[i].reviews_count]);
7 | }
8 | $(function () {
9 | $('#container').highcharts({
10 | chart: {
11 | type: 'column'
12 | },
13 | title: {
14 | text: 'Popular Beers by Number of Reviews'
15 | },
16 | subtitle: {
17 | text: 'Source: Ratebeer'
18 | },
19 | xAxis: {
20 | type: 'category',
21 | labels: {
22 | rotation: -45,
23 | style: {
24 | fontSize: '13px',
25 | fontFamily: 'Verdana, sans-serif'
26 | }
27 | }
28 | },
29 | yAxis: {
30 | min: 0,
31 | title: {
32 | text: 'Number of Reviews'
33 | }
34 | },
35 | legend: {
36 | enabled: false
37 | },
38 | tooltip: {
39 | pointFormat: 'Number of Reviews: {point.y}'
40 | },
41 | series: [{
42 | name: 'Beer',
43 | data: beersAndReviews,
44 | dataLabels: {
45 | enabled: true,
46 | rotation: -90,
47 | color: '#FFFFFF',
48 | align: 'right',
49 | format: '{point.y}',
50 | y: 10,
51 | style: {
52 | fontSize: '12px',
53 | fontFamily: 'Verdana, sans-serif'
54 | }
55 | }
56 | }]
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/js/chart3.js:
--------------------------------------------------------------------------------
1 | var url = "http://localhost:3000/api/v1/ratings/1?compare=2.jsonp?callback=?";
2 |
3 | $.getJSON(url, function (json) {
4 | var ratings = new Array;
5 | var count = json.ratings.length;
6 | for (i = 0; i < count; i++) {
7 | ratings.push([json.ratings[i].user1_rating, json.ratings[i].user2_rating]);
8 | }
9 | $(function () {
10 | $('#container').highcharts({
11 | chart: {
12 | type: 'scatter',
13 | zoomType: 'xy'
14 | },
15 | title: {
16 | text: 'Comparing Beer Preferences of Two Users'
17 | },
18 | subtitle: {
19 | text: 'Source: Ratebeer'
20 | },
21 | xAxis: {
22 | title: {
23 | enabled: true,
24 | text: 'User1'
25 | },
26 | startOnTick: false,
27 | endOnTick: true,
28 | showLastLabel: true
29 | },
30 | yAxis: {
31 | title: {
32 | text: 'User2'
33 | },
34 | startOnTick: true
35 | },
36 | legend: {
37 | layout: 'vertical',
38 | align: 'left',
39 | verticalAlign: 'top',
40 | x: 100,
41 | y: 70,
42 | floating: true,
43 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF',
44 | borderWidth: 1
45 | },
46 | plotOptions: {
47 | scatter: {
48 | marker: {
49 | radius: 5,
50 | states: {
51 | hover: {
52 | enabled: true,
53 | lineColor: 'rgb(100,100,100)'
54 | }
55 | }
56 | },
57 | states: {
58 | hover: {
59 | marker: {
60 | enabled: false
61 | }
62 | }
63 | },
64 | tooltip: {
65 | headerFormat: '{series.name} ',
66 | pointFormat: 'user1: {point.x}, user2: {point.y}'
67 | }
68 | }
69 | },
70 | series: [{
71 | name: 'Beer Data',
72 | color: 'rgba(223, 83, 83, .5)',
73 | data: ratings
74 | }]
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------