title: Bokeh Dynamic Axis Scaling
body: <p>My question is similar to <a href="https://stackoverflow.com/questions/41177455/bokeh-python-how-to-dynamically-set-values-for-x-axis-and-y-axis">this question</a> but I have code examples. I have created a bokeh chart in a Django app that plots times swam in competitive swimming events over time, and uses one plot </p>
  
  <pre><code>plot = figure(
          title='Event Progress',
          x_axis_label='Date',
          y_axis_label='Time',
          x_axis_type='datetime',
          plot_width=400,
          plot_height=200,
          tools=tools,
          responsive=True,
      )
  </code></pre>
  
  <p>the <code>Select</code> widget, and a <code>CustomJS</code> function to only display one line at a time. The problem is, if one event has a time of 10 minutes and another has a time of 25 seconds, the y-axis (time) scales so that both will be seen when they are made visible. I declare each line like</p>
  
  <pre><code>plot_lines[event] = plot.line('x_'+event, 
                                'y_'+event, 
                                line_width=4, 
                                source=source)
  </code></pre>
  
  <p>and the <code>Select</code> widget like</p>
  
  <pre><code>multi_select = Select(title="Select Event:", 
                        value=events[0], 
                        options=events, 
                        callback=callback)
  </code></pre>
  
  <p>if that's helpful. I'm also custom formatting the y-axis as </p>
  
  <pre><code>plot.yaxis.formatter = FuncTickFormatter(code="""
      return Math.floor(tick/60) + ":" + tick.toFixed(2)
  """)
  </code></pre>
  
  <p>so that it displays the time correctly. Everything else is just formatting the data, setting the lines to visible or not and the callback function. Is there a way to dynamically scale a single axis for each line? I suppose ideally I could set the y-max and y-min for each line to keep it within the chart bounds, but I'm not sure if that's possible or what my other options are.</p>
  
  <p>Update: Here's my complete code.</p>
  
  <pre><code>def graph_event(swimmer):
  hover_tool = date_time_hover_tool()
  tools = ['pan', 'box_zoom', hover_tool, 'reset', 'save']
  plot = figure(
      title='Event Progress',
      x_axis_label='Date',
      y_axis_label='Time',
      x_axis_type='datetime',
      plot_width=400,
      plot_height=200,
      tools=tools,
      responsive=True,
  )
  # format datetime.timedelta objects to MM:ss.mm
  plot.yaxis.formatter = FuncTickFormatter(code=
      """
      return Math.floor(tick/60) + ":" + tick.toFixed(2)
      """
  )
  
  data_source = {}
  events = []
  first_event = None
  for event in EVENT_CHOICE:
      e = '_'.join([word.lower() for word in event[0].split()])
  
      results = Event.objects.filter(swimmer=swimmer).filter(event=event[0]).order_by('date')
      if results.exists():
          if len(results) == 1: # one point will not display well on graph
              data_source['x_'+e] = None
              data_source['y_'+e] = None
              data_source['date_'+e] = None
              data_source['time_'+e] = None
              continue
  
          events.append(event[1])
          if first_event == None:
              first_event = e
  
          x, y = [], []
          date, time = [], []
          for r in results.iterator():
              d = r.date
              t = r.time.total_seconds()
              x.append(d)
              y.append(t)
              date.append(d.strftime('%m/%d/%y')) # date to string for hover
              time.append('{:d}:{:.2f}'.format(int(t)/60, t)) # time to string for hover
  
          data_source['x_'+e] = x
          data_source['y_'+e] = y
          data_source['date_'+e] = date
          data_source['time_'+e] = time
  
      else:
          # eliminates KeyError exceptions
          data_source['x_'+e] = None
          data_source['y_'+e] = None
          data_source['date_'+e] = None
          data_source['time_'+e] = None
  
  # set initial graph
  source = ColumnDataSource(data=dict(
      x=data_source['x_'+first_event],
      y=data_source['y_'+first_event],
      date=data_source['date_'+first_event],
      time=data_source['time_'+first_event]
  ))
  plot.line('x', 'y', source=source)
  
  try:
      select = Select(title="Select Event:", value=events[0], options=events)
  except IndexError:
      return None, None
  
  # callback modifies data source depending on Select box
  callback = CustomJS(args=dict(source=source, select=select), code="""
          data = %s;
  
          if (select.value == "50 Freestyle") {
              source.data['x'] = data.x_50_free;
              source.data['y'] = data.y_50_free;
              source.data['date'] = data.date_50_free;
              source.data['time'] = data.time_50_free;
          } else if (select.value == "100 Freestyle") {
              source.data['x'] = data.x_100_free;
              source.data['y'] = data.y_100_free;
              source.data['date'] = data.date_100_free;
              source.data['time'] = data.time_100_free;
          } else if (select.value == "200 Freestyle") {
              console.log(select.value);
              source.data['x'] = data.x_200_free;
              source.data['y'] = data.y_200_free;
              source.data['date'] = data.date_200_free;
              source.data['time'] = data.time_200_free;
          } else if (select.value == "500 Freestyle") {
              source.data['x'] = data.x_500_free;
              source.data['y'] = data.y_500_free;
              source.data['date'] = data.date_500_free;
              source.data['time'] = data.time_500_free;
          } else if (select.value == "1000 Freestyle") {
              source.data['x'] = data.x_1000_free;
              source.data['y'] = data.y_1000_free;
              source.data['date'] = data.date_1000_free;
              source.data['time'] = data.time_1000_free;
          } else if (select.value == "50 Backstroke") {
              source.data['x'] = data.x_50_back;
              source.data['y'] = data.y_50_back;
              source.data['date'] = data.date_50_back;
              source.data['time'] = data.time_50_back;
          } else if (select.value == "100 Backstroke") {
              source.data['x'] = data.x_100_back;
              source.data['y'] = data.y_100_back;
              source.data['date'] = data.date_100_back;
              source.data['time'] = data.time_100_back;
          } else if (select.value == "200 Backstroke") {
              source.data['x'] = data.x_200_back;
              source.data['y'] = data.y_200_back;
              source.data['date'] = data.date_200_back;
              source.data['time'] = data.time_200_back;
          } else if (select.value == "50 Breaststroke") {
              source.data['x'] = data.x_50_breast;
              source.data['y'] = data.y_50_breast;
              source.data['date'] = data.date_50_breast;
              source.data['time'] = data.time_50_breast;
          } else if (select.value == "100 Breaststroke") {
              source.data['x'] = data.x_100_breast;
              source.data['y'] = data.y_100_breast;
              source.data['date'] = data.date_100_breast;
              source.data['time'] = data.time_100_breast;
          } else if (select.value == "200 Breaststroke") {
              source.data['x'] = data.x_200_breast;
              source.data['y'] = data.y_200_breast;
              source.data['date'] = data.date_200_breast;
              source.data['time'] = data.time_200_breast;
          } else if (select.value == "50 Butterfly") {
              source.data['x'] = data.x_50_fly;
              source.data['y'] = data.y_50_fly;
              source.data['date'] = data.date_50_fly;
              source.data['time'] = data.time_50_fly;
          } else if (select.value == "100 Butterfly") {
              source.data['x'] = data.x_100_fly;
              source.data['y'] = data.y_100_fly;
              source.data['date'] = data.date_100_fly;
              source.data['time'] = data.time_100_fly;
          } else if (select.value == "200 Butterfly") {
              source.data['x'] = data.x_200_fly;
              source.data['y'] = data.y_200_fly;
              source.data['date'] = data.date_200_fly;
              source.data['time'] = data.time_200_fly;
          } else if (select.value == "100 IM") {
              source.data['x'] = data.x_100_im;
              source.data['y'] = data.y_100_im;
              source.data['date'] = data.date_100_im;
              source.data['time'] = data.time_100_im;
          } else if (select.value == "200 IM") {
              source.data['x'] = data.x_200_im;
              source.data['y'] = data.y_200_im;
              source.data['date'] = data.date_200_im;
              source.data['time'] = data.time_200_im;
          } else if (select.value == "400 IM") {
              source.data['x'] = data.x_400_im;
              source.data['y'] = data.y_400_im;
              source.data['date'] = data.date_400_im;
              source.data['time'] = data.time_400_im;
          } else if (select.value == "Base Freestyle") {
              source.data['x'] = data.x_base_free;
              source.data['y'] = data.y_base_free;
              source.data['date'] = data.date_base_free;
              source.data['time'] = data.time_base_free;
          } else if (select.value == "Base Backstroke") {
              source.data['x'] = data.x_base_back;
              source.data['y'] = data.y_base_back;
              source.data['date'] = data.date_base_back;
              source.data['time'] = data.time_base_back;
          } else if (select.value == "Base Breaststroke") {
              source.data['x'] = data.x_base_breast;
              source.data['y'] = data.y_base_breast;
              source.data['date'] = data.date_base_breast;
              source.data['time'] = data.time_base_breast;
          } else if (select.value == "Base Butterfly") {
              source.data['x'] = data.x_base_fly;
              source.data['y'] = data.y_base_fly;
              source.data['date'] = data.date_base_fly;
              source.data['time'] = data.time_base_fly;
          } else if (select.value == "Base IM") {
              source.data['x'] = data.x_base_im;
              source.data['y'] = data.y_base_im;
              source.data['date'] = data.date_base_im;
              source.data['time'] = data.time_base_im;
          }
  
          source.change.emit()
  """ % json.dumps(data_source, cls=DatetimeEncoder))
  
  select.callback = callback
  
  return components(column(select, plot, responsive=True))
  </code></pre>
  
  <p>And here's the hover tool and JSON datetime serializer.</p>
  
  <pre><code>class DatetimeEncoder(json.JSONEncoder):
  """
  Encodes Python datetime.date objects to make compatible with JSON serialization.
  """
  def default(self, obj):
      try:
          return super(DatetimeEncoder, obj).default(obj)
      except TypeError:
          return str(obj)
  
  def date_time_hover_tool():
      """
      Generates the HTML for the Bokeh's hover data tool on our graph.
      """
      hover_html = """
        &lt;div&gt;
          &lt;span class="hover-tooltip"&gt;@date&lt;/span&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;span class="hover-tooltip"&gt;@time&lt;/span&gt;
        &lt;/div&gt;
      """
      return HoverTool(tooltips=hover_html)
  </code></pre>
  
  <p>Hopefully this isn't too confusing. Basically I'm looping over each event for that athlete and getting the data if it exists, then in the callback just setting the source data.</p>