Skip to content

Commit

Permalink
Continue graphing project example.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdeisenberg committed Mar 30, 2017
1 parent 0783f2d commit 32d4411
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 0 deletions.
83 changes: 83 additions & 0 deletions _sources/closures.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.. Copyright © J David Eisenberg
.. |---| unicode:: U+2014 .. em dash, trimming surrounding whitespace
:trim:

Graphing Program: Drawing
'''''''''''''''''''''''''''

All of the functions that do the actual drawing of the graph (axes, lines, dots, etc.), need to convert graph coordinates (*x*, y*) to the corresponding canvas coordinates, so the program needs a function like ``(to-screen [[x y]])``. But wait |---| this function needs to know the minimum and maximum *x* and *y* as well as the width and height of the canvas. You don’t want to “hard code” the numbers into the function for a particular canvas size (300 x 200) and graph dimensions (*x* from 0 to 7 and *y* from 0 to 25)::

(defn to-screen [[x y]]
(let [sx (+ (* (/ (* 0.85 300) 7) x) (* 0.075 300))
sy (+ (* (/ (* 0.85 200) 25) y) (* 0.075 200))]
[sx sy]))

That would be good for only this one particular graph, and you might very well want to have more than one graph on a page. On the other hand, you don’t want to have to pass in all that information every time you do a conversion::

(defn to-screen[[x y] [min-x max-x] [min-y max-y] [w h]]
(let [sx (+ (* (/ (* 0.85 (- max-x min-x))) (- x min-x)) (* 0.075 w))
sy (+ (* (/ (* 0.85 (- min-y max-y))) (- y max-y)) (* 0.075 h))]
[sx sy]))

Surely there must be a better way! `Insert obligatory Airplane joke here.<http://www.vulture.com/2016/01/airplane-dont-call-me-shirley.html>`_ And indeed there is. You write a function that takes all those parameters and returns a conversion function to you. That’s the ``make-convert`` function in the following code:

.. activecode:: make_function
:language: clojurescript

(defn make-convert [[min-x max-x] [min-y max-y] [w h]]
(let [mx (/ (* 0.85 w) (- max-x min-x))
my (/ (* 0.85 h) (- min-y max-y))]
;; return anonymous function
(fn [[x y]]
[(+ (* mx (- x min-x)) (* 0.075 w))
(+ (* my (- y max-y)) (* 0.075 h))])))

(def to-screen (make-convert [0 7] [0 25] [300 200]))
(to-screen [0 0])

At this point, you may have an objection: “Hold on a second. The symbols ``mx`` and ``my`` are `local bindings </local_syms.rst>`_ that don’t exist outside of ``make-convert``. Once it finishes returning the anonymous function, dom’t those bindings vanish?” Ordinarily, this is the case. However, when a function has access to symbols defined outside its scope (in this case, ``mx``, ``min-x``, etc.), those symbols do not vanish. Their values at the time the anonymous function was created are “locked in” to the definition and remain available. In computer science, this is referred to as a **closure** (not to be confused with the Clojure in ClojureScript). To paraphrase Wikipedia, a closure is a record storing a function together with the environment in which it was created.

This solves the problem nicely. You can create a ``to-screen`` function for any size canvas or graph we want and just pass that function as one extra argument to all the drawing functions rather than passing a boatload of arguments at every call.

There is one other item that needs to be passed to all the drawing functions: the canvas’s drawing context. Rather than creating another parameter, this code creates a map that has the context and the conversion function in it. This code also summarize what has been done up to this point::

(def temperatures [[3 9] [2 13] [4 10] [4 9] [4 12] [9 20] [16 21]])

(defn splitter
"Split temperatures into vectors of [index min]... and
[index max]...."
[data]
[(into [] (map-indexed
(fn [index [low hi]]
[(inc index) hi] data)))
(into [] (map-indexed
(fn [index [low hi]]
[(inc index) low]) data))])

(defn make-convert
"Create a function to create graph x-y coordinates
to screen coordinates in a canvas that is w by h"
[[min-x max-x] [min-y max-y] w h]
(let [mx (/ (* 0.85 w) (- max-x min-x))
my (/ (* 0.85 h) (- min-y max-y))]
(fn [[x y]]
[(+ (* mx (- x min-x)) (* 0.075 w))
(+ (* my (- y max-y)) (* 0.075 h))])))

(defn make-map
"Given an x and y range and a canvas ID, create
a map that has the canvas contet and a function that
will convert x/y to canvas coordinates."
[[min-x max-x][min-y max-y] canvasId]
(let [canvas (.getElementById js/document canvasId)
w (.-width canvas)
h (.-height canvas)]
{:ctx (.getContext canvas "2d")
:to-screen (make-convert [min-x max-x] [min-y max-y] w h)}))

(defn find-min-max
"Return the lowest and highest values from the source data."
[data]
(reduce (fn [[acc-min acc-max] [low high]])
[(if (< low acc-min) low acc-min)]
(if (> high acc-max) high acc-max) (first data) (rest data)))
26 changes: 26 additions & 0 deletions _sources/coordinates.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.. Copyright © J David Eisenberg
.. |---| unicode:: U+2014 .. em dash, trimming surrounding whitespace
:trim:

Converting Coordinates
'''''''''''''''''''''''''

So here’s the next problem to solve: the canvas uses a coordinate system with the *y*-axis whose direction is opposite that of the (cartesian coordinate) graph, and the dimensions of the graph and the canvas are very different.

You also will need some room for the axis labels on the graph. So, what I decided to do was to have a margin that is 7.5% of the height of the canvas at top and bottom, and a margin that is 7.5% of the width of the canvas at the right and left. Given that, how do you convert from graph coordinates to screen coordinates?

Unless you have truly superb powers of visualization and calculation, you probably can’t derive the formulas for this conversion in your head; I certainly couldn’t. When I was young, my mom often would ask me, “Do I need to draw you a diagram?” In this case, the answer is “Yes, I need a diagram |---| with numbers on it |---| to solve this problem.” Here are the diagrams and the resulting calculations, with the graph coordinates in black and the screen coordinates in red.


.. image:: images/graphing/graph_drawing.jpg
:alt: Drawing of graph showing both screen and canvas coordinates

.. image:: images/graphing/x_conversion.jpg
:alt: Formula for slope and intercept of x-coordinate conversion

.. image:: images/graphing/y_conversion.jpg
:alt: Formula for slope and intercept of y-coordinate conversion

What? You were expecting some polished, fancy drawings and beautifully typeset equations? This is me, planning. If I weren’t writing this book, nobody would have seen the derivation of the formulas except me.

I’ll come back to these formulas later during the program. On the next page, I’ll examine the general flow of the program itself.
83 changes: 83 additions & 0 deletions _sources/graphing_flow.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.. Copyright © J David Eisenberg
.. |---| unicode:: U+2014 .. em dash, trimming surrounding whitespace
:trim:

Graphing Program: Outline
'''''''''''''''''''''''''''

As a reminder, the object is to transform this data::

[[3 9] [2 13] [4 10] [4 9] [4 12] [9 20] [16 21]]
into this graph:

.. image:: images/temperature_graph.png
:alt: Line graph showing max temperature as red line and min temperature as blue line

The program will have to:

* Split the temperatures into two sets of *x*, *y* pairs to be graphed::

[[[1 3] [2 2] [3 4] [4 4] [5 4] [6 9] [7 16]]
[[1 9] [2 13] [3 10] [4 9] [5 12] [6 20] [7 21]]]
* Draw the axes:

* Draw the lines
* Draw the tick marks
* Draw the labels

* Draw a connected line for the minimum teperatures
* Draw dots for the minimum temperatures
* Draw a connected line for the maximum temperatures
* Draw dots for the maximum temperatures

Before moving on to all the drawing, let’s do the splitting. The trick here is to use the ``map-indexed`` function. It works like ``map``, except that the function you provide has two parameters: the “index number” and the item from the collection being mapped. Here is an example that takes a list of words and returns a vector of upper-cased words and their position in the list (starting at zero):

.. activecode:: map_indexed_example
:language: clojurescript

(defn counted-upper [index item]
(vector index (.toUpperCase item)))

(map-indexed counted-upper ["Cat" "DoG" "birD"])

Now, see if you can do the split of the temperatures. Hint: use ``map-indexed`` twice. The first time, extract the first item in the temperature pair (the low temperature). The second time, extract the second item in the temperature pair. Remember that the index stars at zero, so you will want to add one; you can use the ``inc`` function for that.

.. container:: full_width

.. tabbed:: q1

.. tab:: Question

.. activecode:: split_indexed_question
:language: clojurescript

(def temperatures [[3 9] [2 13] [4 10] [4 9] [4 12] [9 20] [16 21]])

(defn splitter[data]
; your code here
)

(splitter temperatures)

.. tab:: Answer

.. activecode:: split_indexed_answer
:language: clojurescript

(def temperatures [[3 9] [2 13] [4 10] [4 9] [4 12] [9 20] [16 21]])

(defn index-min [index temperature-pair]
[(inc index) (first temperature-pair)])

(defn index-max [index temperature-pair]
[(inc index) (last temperature-pair)])

(defn splitter [data]
[(into [] (map-indexed index-min data))
(into [] (map-indexed index-max data))])

(splitter temperatures)


2 changes: 2 additions & 0 deletions _sources/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Project 1 - Graphing
canvas.rst
canvas2.rst
coordinates.rst
graphing_flow.rst
closures.rst

Appendices
==========
Expand Down

0 comments on commit 32d4411

Please sign in to comment.