In this lab we will build out a visualization library to help us plot visualizations with plotly in the future.
Let's start with a function called plot
which plots our data.
import plotly
from plotly.offline import iplot, init_notebook_mode
init_notebook_mode(connected=True)
def plot(figure):
plotly.offline.iplot(figure)
Now, to see our plot on the screen, we need to provide our plot
function a dictionary, with a key of data
and the corresponding value as an array of traces.
sample_trace = {'x': [1, 2, 3], 'y': [2, 3, 4]}
other_sample_trace = {'x': [3, 4, 3], 'y': [5, 3, 4]}
sample_figure = {'data': [sample_trace, other_sample_trace], 'layout': {'title': 'Our sample plot'}}
plot(sample_figure)
Ok, so for our plot
function to work, we would like an easy way to create figures, an easy way to create layouts, and an easy way to create traces. Let's take these one by one, and start with a build_trace
function that easily creates traces.
So it should be able to take data that comes in the following format:
data = [{'x': 1, 'y': 1}, {'x': 3, 'y': 2}, {'x': 2, 'y': 5}]
And if you remember the format of our trace, each trace should look like the following format:
sample_trace = {'x': [1, 2, 3], 'y': [2, 3, 4]}
So write a function called trace
that takes in an array of datapoints and returns a dictionary with a key of x
that points to an array of x values, and a key of y
that points to an array of y values.
def build_trace(data, mode = 'markers', name = 'data'):
x_values = list(map(lambda datapoint: datapoint['x'], data))
y_values = list(map(lambda datapoint: datapoint['y'], data))
return {'x': x_values, 'y': y_values, 'mode': mode, 'name': name}
build_trace(data)
# {'x': [1, 3, 2], 'y': [1, 2, 5]}
{'mode': 'markers', 'name': 'data', 'x': [1, 3, 2], 'y': [1, 2, 5]}
We want our build_trace
function to take an argument named mode, that will set the mode of the trace. So the value passed through as the mode argument, should correspond to the value the mode
key in our returned dictionary.
build_trace(data, 'markers')
# {'mode': 'markers', 'x': [1, 3, 2], 'y': [1, 2, 5]}
{'mode': 'markers', 'name': 'data', 'x': [1, 3, 2], 'y': [1, 2, 5]}
By default, our mode should be set to markers. So have the mode
argument have a default value set to 'markers'.
build_trace(data)
# {'mode': 'markers', 'x': [1, 3, 2], 'y': [1, 2, 5]}
{'mode': 'markers', 'name': 'data', 'x': [1, 3, 2], 'y': [1, 2, 5]}
Finally, we want to be able to set the name of the trace. This will be useful for labeling traces when we have more than one. We need an argument named name
, that sets the name of the trace. So the value passed through as the name
argument, should correspond to the value of the name
key in our returned dictionary.
build_trace(data, 'markers', 'sample plot')
# {'mode': 'markers', 'name': 'sample plot', 'x': [1, 3, 2], 'y': [1, 2, 5]}
{'mode': 'markers', 'name': 'sample plot', 'x': [1, 3, 2], 'y': [1, 2, 5]}
Ok, now we have built a function to easily generate a trace. Let's see it in action! Uncomment some of the code below and try it out! Experiment with having your trace display with 'markers' or 'lines'. You can only see the name of the plot if more than one trace is present, so practice adding more than one trace, to the plot.
# trace0 = build_trace(data)
# trace0 = build_trace(data, 'lines')
# trace1 = build_trace(data, 'lines', 'my_trace')
# plot({'data':[trace0, trace1]})
Now let's make one more function to create a trace, called trace_values
. It should work just like our build_trace
method, except that it should take an list of x_values and a list of y_values.
def trace_values(x_values, y_values, mode = 'markers', name="data"):
return {'x': x_values, 'y': y_values, 'mode': mode, 'name': name}
trace_values([1, 2, 3], [2, 4, 5])
# {'x': [1, 2, 3], 'y': [2, 4, 5]}
{'mode': 'markers', 'name': 'data', 'x': [1, 2, 3], 'y': [2, 4, 5]}
Just like our build_trace
function, the trace_values
function should allow us to take in an argument of mode
, and name
to set the value to the mode
key, and the value to the name
key in our returned dictionary.
trace_values([1, 2, 3], [2, 4, 5], 'line', 'line trace')
# {'mode': 'line', 'name': 'line trace', 'x': [1, 2, 3], 'y': [2, 4, 5]}
{'mode': 'line', 'name': 'line trace', 'x': [1, 2, 3], 'y': [2, 4, 5]}
From there, we can use our trace_values
function to plot our chart.
# trace2 = trace_values([1, 2, 3], [2, 4, 5], 'line', 'line trace')
# plot({'data': [trace2]})
Ok, now that we have built some functions to create traces, let's write a function to create a layout. Remember that our layout also can be passed to our plot function.
# plot({'data': [trace0, trace2], 'layout': {'title': 'Sample Title'}})
Ok, in building a layout, we should also return a dictionary.
def build_layout(x_range = None, y_range = None, options = {}):
layout = {}
if isinstance(x_range, list): layout.update({'xaxis': {'range': x_range}})
if isinstance(y_range, list): layout.update({'yaxis': {'range': y_range}})
layout.update(options)
return layout
build_layout()
# {}
{}
Oftentimes in building a layout, we want an easy way to set the range for the x
and y
axis. To set a range in the x-axis of
{'xaxis': {'range': [1, 4]}, 'yaxis': {'range': [2, 5]}}
Add an argument of x_range returns a dictionary with a range set on the xaxis.
build_layout([1, 4])
# {'xaxis': {'range': [1, 4]}}
{'xaxis': {'range': [1, 4]}}
To have functionality that when no x_range is provided, a blank dictionary is still returned. Have the x_range
have a default argument that sets x_range
to None
. So only modify the returned layout when the x_range
does not equal None
.
build_layout()
{}
Now modify the function so that the same functionality is provided for the y_range
. When the y_range
is provided it adds a key of yaxis which points to a dictionary that expreses the yaxis range.
build_layout([1, 3], [4, 5])
# {'xaxis': {'range': [1, 3]}, 'yaxis': {'range': [4, 5]}}
{'xaxis': {'range': [1, 3]}, 'yaxis': {'range': [4, 5]}}
Now have the final argument of the function be options. The options
argument should by default point to a dictionary. And whatever is provided as pointing to the options argument should be updated into the returned dictionary.
build_layout(options = {'title': 'foo'})
{'title': 'foo'}
build_layout([1, 3], options = {'foo': 'bar'})
# {'foo': 'bar', 'xaxis': {'range': [1, 3]}}
{'foo': 'bar', 'xaxis': {'range': [1, 3]}}
Ok, now let's see this build_layout
function in action.
another_trace = trace_values([1, 2, 3], [6, 3, 1])
another_layout = build_layout([-1, 4], [0, 7], {'title': 'Going Down...'})
# plot({'data': [another_trace], 'layout': another_layout})
Finally, we'll modify the plot
function for you so that it takes the data, and the layout as arguments.
def plot(traces = [], layout = {}):
if not isinstance(traces, list): raise TypeError('first argument must be a list. Instead is', traces)
if not isinstance(layout, dict): raise TypeError('second argument must be a dict. Instead is', layout)
plotly.offline.iplot({'data': traces, 'layout': {}})
# trace4 = trace_values([4, 5, 6], [10, 5, 1], mode = 'lines')
# layout = build_layout(options = {'title': 'The big picture'})
# plot([trace4], layout)