#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
| `@purpose`: Hub for creating data visualizations using pandas, seaborn, and bokeh.  
| `@date`: Created on Sat May 1 15:12:38 2019  
| `@author`: Semeon Risom  
| `@email`: semeon.risom@gmail.com  
| `@url`: https://semeon.io/d/imhr
"""

# available functions
__all__ = ['Plot']

# core
from pdb import set_trace as breakpoint
from distutils import dir_util
import importlib
import datetime
import shutil
import sys
import os

# data
import pandas as pd
import numpy as np

# seaborn
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt

# local libraries
from .. import settings

# constants
console = settings.console
debug = settings.debug

class Plot():
	"""Hub for creating data visualizations using pandas, seaborn, and bokeh."""
	@classmethod
	def __init__(self, isLibrary=False):
		"""Hub for creating data visualizations using pandas, seaborn, and bokeh.

		Parameters
		----------
		isLibrary : :obj:`bool`
			Check if required libraries are available.
		"""
		#check libraries
		if isLibrary:
			settings.library(__required__)

	@classmethod
	def __font__(cls):
		"""Add Helvetica to matplotlib."""
		import imhr
		from pathlib import Path
		from matplotlib import matplotlib_fname, rcParams
		import matplotlib.font_manager as font_manager

		directory = matplotlib_fname().replace("/matplotlibrc", "")
		root = Path(imhr.__file__).parent
		source_ = "%s/dist/resources/Helvetica.ttf"%(root)
		destination_ = f'{directory}/fonts/ttf'

		#add to matplotlib font folder
		shutil.copy(source_, destination_)

		#add to computer font folder
		##if running osx
		if sys.platform == "darwin":
			shutil.copy(source_, '/Library/Fonts/')
		##if running win32
		if sys.platform == "win32":
			shutil.copy(source_, 'c:\\windows\\fonts')

		#rebuild fonts
		prop = font_manager.FontProperties(fname=source_)
		prop.set_weight = 'light'
		rcParams['font.family'] = prop.get_name()
		rcParams['font.weight'] = 'light'
		font_manager._rebuild()

	@classmethod
	def bokeh_trial(cls, config, df, stim_bounds, roi_bounds, flt):
		"""Create single subject trial bokeh plots.

		Parameters
		----------
		df : :class:`pandas.DataFrame`
			Pandas dataframe of participant sample data.
		stim_bounds : :class:`dict`
			Stimulus bounds on screen.
		roi_bounds : :class:`dict`
			ROI bounds on screen.
		flt : :class:`str`
			Filter type.
		"""
		#----bokeh
		from bokeh.core.properties import value
		from bokeh.plotting import reset_output, figure
		from bokeh.models import HoverTool, Range1d, BoxAnnotation, ColumnDataSource, CDSView, BooleanFilter
		from bokeh.layouts import gridplot
		from bokeh.embed import components

		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		#metadata
		emotion = [df['left_mood'][0],df['right_mood'][0]]
		#filter xy coordinates
		flt_x = '%s_x'%(flt)
		flt_y = '%s_y'%(flt)
		
		#get display size
		xy = [int(i) for i in (df['monitorSize.px'].values[0]).split('x')]
		monitorSize = xy
		
		#subject data
		t = df['timestamp'].values
		
		#get fixations
		df['fixation_onset'] = df.loc[df.groupby('fix_num')['%s_fix_all'%(flt)].head(1).index, '%s_fix_all'%(flt)]
		df['fixation_onset'] = df.apply(lambda x: True if (x['fixation_onset'] == True) else False, axis=1)
		
		#source for syncing data
		s = ColumnDataSource(df)

		##tools = "box_select,save,pan,box_zoom,reset"
		tools="box_select,save,reset"

		#figure 1
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		p1 = figure(
				output_backend="webgl",
				tools=tools,
				plot_width=1200,
				plot_height=400,
				x_range=Range1d(-100, (t[-1]+100), bounds="auto"),
				y_range=Range1d(0,(monitorSize[0]+100), bounds="auto"),
				x_axis_label = 'time (msec)', y_axis_label = 'x/y (px)',
				sizing_mode="stretch_both",
		)

		#hover
		hover = HoverTool()
		hover.tooltips = [
			("name", "$name"),
			("sample", "$index"),
			("data (x,y)", "($x{(0)},$y{(0)})")
		]
		p1.tools.append(hover)
		p1.hover.line_policy = "nearest"

		#graph
		###-----------------------------------------------------------------x=time, y=(x or y)
		#original x
		p1.scatter(x='timestamp', y='x', color='#2965C0', alpha=0, line_width=2,
				line_dash='dashed', name='raw_x', source=s)
		p1.line(x='timestamp', y='x', color='#2965C0', alpha=0.5, line_width=2,
				legend=value('raw_x'), line_dash='dashed', name='raw_x', source=s)
		#filtered x
		p1.scatter(x='timestamp', y=flt_x, color='#265EB3', alpha=0, line_width=2,
				name=flt_x, source=s)
		p1.line(x='timestamp', y=flt_x, color='#265EB3', alpha=1, line_width=2,
				legend=value(flt_x), name=flt_x, source=s)
		#peak removed
		#p1.line(x=t, y=sg_x, color='#101f23', alpha=1, line_width=2,legend='x_peaksg', name='x_peaksg')

		#original y
		p1.scatter(x='timestamp', y='y', color='#C01F1F', alpha=0, line_width=2,
				line_dash='dashed', name='raw_y', source=s)
		p1.line(x='timestamp', y='y', color='#C01F1F', alpha=0.5, line_width=2,
				legend=value('raw_y'), line_dash='dashed', name='raw_y', source=s)
		#filtered y
		p1.scatter(x='timestamp', y=flt_y, color='#A61B1B', alpha=0, line_width=2,
				name=flt_y, source=s)
		p1.line(x='timestamp', y=flt_y, color='#A61B1B', alpha=1, line_width=2,
				legend=value(flt_y), name=flt_y, source=s)

		#task-events (stimulus onset, dotloc onset)
		##stimulus onset
		time = df.loc[df['marker']=='Stimulus Onset']['timestamp'].iloc[0]
		box = BoxAnnotation(left=time, right=time + 10, fill_alpha=1, fill_color='#434e54', level='glyph')
		p1.add_layout(box)
		##dotloc onset
		time = df.loc[df['marker']=='Dotloc Onset']['timestamp'].iloc[0]
		box = BoxAnnotation(left=time, right=time + 10, fill_alpha=1, fill_color='#434e54', level='glyph')
		p1.add_layout(box)

		#fixations
		for index, row in df[~df['sg_fix_all'].isin([False])].groupby('fix_num'):
			start = row.iloc[0]['timestamp']
			end = row.iloc[-1]['timestamp']
			box = BoxAnnotation(left=start, right=end,fill_alpha=0.2, fill_color='#9e9e9e', level='glyph')
			p1.add_layout(box)
		del index, row

		#legend
		p1.toolbar.logo = None
		p1.legend.orientation = "horizontal"
		p1.legend.location = "top_left"
		p1.legend.click_policy = "hide"

		###-----------------------------------------------------------------window display
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		#display what participant sees
		tools = "box_select,save,pan,box_zoom,reset"
		p2 = figure(
				output_backend="webgl",
				tools=tools,
				plot_width=1200,
				plot_height=600,
				x_axis_label = 'x (px)', y_axis_label = 'y (px)',
				x_range=Range1d(0, monitorSize[0], bounds="auto"),
				y_range=Range1d(0, monitorSize[1], bounds="auto"),
				sizing_mode="stretch_both"
		)
		#hover
		hover = HoverTool()
		hover.tooltips = [
			("name", "$name"),
			("sample", "$index"),
			("data (x,y)", "($x{(0)},$y{(0)})")
		]
		p2.tools.append(hover)
		p2.hover.line_policy = "nearest"
		###---------line plots
		#------gaze xy
		p2.cross(x=flt_x, y=flt_y, size=10, color="firebrick", alpha=1, fill_alpha=0.6,
				fill_color="firebrick", source=s, name='gaze', legend='gaze')

		#------fixation onset within and outside roi
		#get booleanfilter
		l_fix_all = [True if x !=False else False for x in s.data['fixation_onset']]
		fix_all = BooleanFilter(l_fix_all)
		view = CDSView(source=s, filters=[fix_all])
		##draw square ##2222b2
		p2.square(x=flt_x, y=flt_y, size=10, color="#2222b2", alpha=1, fill_alpha=0.6,
				fill_color="white", source=s, view=view, name='fixation', legend=value("fixation"))
		
		#------fixation within and outside roi
		#get booleanfilter
		l_fix_false = [True if x !=False else False for x in s.data['sg_fix_all']]
		fix_false = BooleanFilter(l_fix_false)
		view = CDSView(source=s, filters=[fix_false])
		##draw square ##2222b2
		p2.square(x=flt_x, y=flt_y, size=10, color="black", alpha=1, fill_alpha=0.6,
				fill_color="white", source=s, view=view, name='false-fixation', legend=value("false-fixation"))

		#------only fixation within roi
		#get booleanfilter
		l_fix_roi = [True if (y==True and z==True) else False for y,z in zip(s.data['fixation_onset'], s.data['dwell'])]
		fix_roi = BooleanFilter(l_fix_roi)
		view = CDSView(source=s, filters=[fix_roi])
		##draw triangle ##198D40
		p2.triangle(x=flt_x, y=flt_y, size=10, color="#198D40", alpha=1, fill_alpha=0.6,
				fill_color="white", source=s, view=view, name='roi', legend=value("roi"))
		
		#------dwell-time gaze within roi
		l_dwell_roi = [True if x !=False else False for x in s.data['dwell']]
		dwell_roi = BooleanFilter(l_dwell_roi)
		view = CDSView(source=s, filters=[dwell_roi])
		##draw circle ##ff9800
		p2.circle(x=flt_x, y=flt_y, size=10, color="#ff9800", alpha=1, fill_alpha=0.6,
				fill_color="white", source=s, view=view, name='dwell', legend=value("dwell"))

		#------stim bounds
		stimBounds = stim_bounds

		#------roi bounds
		roiBounds = roi_bounds

		#emotion
		left, right = emotion
		if left == 'Sad':
			left_color = '#ff0000'
			right_color = '#9e9e9e'
		else:
			right_color = '#ff0000'
			left_color = '#9e9e9e'

		#roi
		#left
		p2.add_layout(BoxAnnotation(left=roiBounds[0]['bx1'], right=roiBounds[0]['bx2'],
									top=roiBounds[0]['by1'], bottom=roiBounds[0]['by2'],
							fill_alpha=0.05, fill_color='blue', level='glyph'))
		#right
		p2.add_layout(BoxAnnotation(left=roiBounds[1]['bx1'], right=roiBounds[1]['bx2'],
									top=roiBounds[1]['by1'], bottom=roiBounds[1]['by2'],
							fill_alpha=0.05, fill_color='blue', level='glyph'))


		#--------stim bounds
		#left
		p2.add_layout(BoxAnnotation(left=stimBounds[0]['bx1'], right=stimBounds[0]['bx2'],
									top=stimBounds[0]['by1'], bottom=stimBounds[0]['by2'],
							fill_alpha=0.3, fill_color=left_color, level='glyph'))
		#right
		p2.add_layout(BoxAnnotation(left=stimBounds[1]['bx1'], right=stimBounds[1]['bx2'],
									top=stimBounds[1]['by1'], bottom=stimBounds[1]['by2'],
							fill_alpha=0.3, fill_color=right_color, level='glyph'))
		#hover
		hover = HoverTool()
		hover.tooltips = [
			("time", "@timestamp"),
			("sample", "$index"),
			("data (x,y)", "($%s_x{(0)},$%s_y{(0)})"%(flt,flt))
		]
		#p2.tools.append(hover)
		#p2.hover.line_policy = "nearest"

		#legend
		p2.toolbar.logo = None
		p2.legend.orientation = "horizontal"
		p2.legend.location = "top_left"
		p2.legend.click_policy = "hide"

		#reverse axis
		p2.x_range = Range1d(0, monitorSize[0])
		p2.y_range = Range1d(monitorSize[1], 0)

		###-----------------------------------------------------------------slider
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		start = df.iloc[0]['timestamp']
		end = df.iloc[-1]['timestamp']
		# callback = CustomJS(args=dict(source=s), code="""
		#     data = source.data;
		#     t = cb_obj;
		#     f = cb_obj.value;
		#     x = data['x'];
		#     y = data['y'];
		#     source.change.emit();
		#     console.log('test');
		# """)
		# p3 = Slider(
		#         title="time",
		#         start=start,
		#         end=end,
		#         value=1, step=1,
		#         callback=callback,
		#         bar_color='red',
		#         width=1200,
		#         show_value=True,
		#         sizing_mode="stretch_both"
		# )

		###-----------------------------------------------------------------combining plots
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		# grid = gridplot(children=[[p1], [p2], [p3]],
		grid = gridplot(children=[[p1], [p2]],
						merge_tools=True,
						toolbar_location='right',
						sizing_mode="fixed")

		#get html
		script, div = components(grid)

		##convert seperate plots and div to single string
		plots = (''.join(map(str, [div, '\n', script])))

		#clear cache
		reset_output()

		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plots

	@classmethod
	def bokeh_calibration(cls, config, df, cxy, event, monitorSize=[1920,1080]):
		"""Create calibration matrix, using pandas and bokeh.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		cxy : :class:`pandas.DataFrame`
			Pandas dataframe of calibration points.
		event : :obj:`string`
			calibration, or validation.
		monitorSize : :obj:`list`
			Monitor size, in pixels.
		"""
		from bokeh.plotting import reset_output, figure
		from bokeh.models import Range1d, ColumnDataSource
		from bokeh.embed import components

		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		#subject data
		event_t = df['event_trial'].values
		
		#source for syncing data
		s = ColumnDataSource(df)

		##tools = "box_select,save,pan,box_zoom,reset"
		#figure 1
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		##tools = "box_select,save,pan,box_zoom,reset"
		tools = "box_select,reset"
		p1 = figure(
				output_backend="webgl",
				tools=tools,
				plot_width=1200,
				plot_height=150,
				x_range=Range1d(0 - 0.1, (event_t[-1] + 0.1), bounds="auto"),
				y_range=Range1d(0,(monitorSize[0]+100), bounds="auto"),
				x_axis_label = 'calibration point', y_axis_label = 'x/y (px)',
				sizing_mode="scale_width",
		)

		#graph
		###-----------------------------------------------------------------x=time, y=(x or y)
		#original x
		p1.scatter(x='event_trial', y='cx', color='#2965C0', alpha=0.5, line_width=2,
				line_dash='dashed', name='cx', source=s)
		# p1.line(x='event_trial', y='cx', color='#2965C0', alpha=0.5, line_width=2,
				# line_dash='dashed', name='cx', source=s,legend=value('cx'))

		#original y
		p1.scatter(x='event_trial', y='cy', color='#C01F1F', alpha=0.5, line_width=2,
				line_dash='dashed', name='cy', source=s)
		# p1.line(x='event_trial', y='cy', color='#C01F1F', alpha=0.5, line_width=2,
		#         line_dash='dashed', name='cy', source=s,legend=value('cy'))

		###-----------------------------------------------------------------window display
		#sizing_mode: ``"fixed"``, ``"stretch_both"``, ``"scale_width"``, ``"scale_height"``, ``"scale_both"``
		##tools = "box_select,save,pan,box_zoom,reset"
		tools="save,reset"
		p2 = figure(
			output_backend="webgl",
			tools=tools,
			plot_width=1200,
			plot_height=600,
			x_axis_label = 'x (px)', y_axis_label = 'y (px)',
			x_range=Range1d(0, monitorSize[0], bounds="auto"),
			y_range=Range1d(0, monitorSize[1], bounds="auto"),
			sizing_mode="scale_width"
		)
		
		###---------line plots
		#----gaze xy
		# cross = p2.cross(x='gx', y='gy', size=10, color="firebrick", alpha=1, fill_alpha=0.6,
		p2.cross(x='gx', y='gy', size=10, color="firebrick", alpha=1, fill_alpha=0.6,
				fill_color="firebrick", source=s, name='gaze')
		#           fill_color="firebrick", source=s, name='gaze', legend=value("gaze"))
		
		#----circle xy
		p2.circle(x=cxy['cx'], y=cxy['cy'], size=12, color="#198D40", alpha=1, fill_alpha=0.6,
				fill_color="white", name='circle')
				#fill_color="white", source=s, name='circle', legend=value("circle"))
				
		# draw mouse click for calibration
		if event=='calibration':
			#----mouse xy
			p2.square(x='mx', y='my', size=10, color="#2222b2", alpha=1, fill_alpha=0.6,
					fill_color="white", source=s, name='mouse')
					#fill_color="white", source=s, name='mouse', legend=value("mouse"))
			
		#----legend
		p1.toolbar.logo = None
		p2.toolbar.logo = None

		#----reverse axis
		p2.x_range = Range1d(0, monitorSize[0])
		p2.y_range = Range1d(monitorSize[1], 0)
		
		#----grid
		#p1
		p1.grid.grid_line_alpha = 0.5
		p1.grid.grid_line_color = "white"
		
		#----font
		# p2
		## title
		p2.axis.axis_label_text_font_style = "normal"
		## labels
		p2.axis.axis_label_text_font_size= "11.5pt"
		p2.axis.axis_label_text_font = "Helvetica"
		p2.axis.axis_label_text_color = "#1c1d1e"
		p2.axis.major_label_text_font_size= "11.5pt"
		p2.axis.major_label_text_font = "Helvetica"
		p2.axis.major_label_text_color = "#1c1d1e"
		# p1
		## title
		p1.axis.axis_label_text_font_style = "normal"
		## labels
		p1.axis.axis_label_text_font_size = "9.5pt"
		p1.axis.axis_label_text_font = "Helvetica"
		p1.axis.axis_label_text_color = "#1c1d1e"
		p1.axis.major_label_text_font_size = "9.5pt"
		p1.axis.major_label_text_font = "Helvetica"
		p1.axis.major_label_text_color = "#1c1d1e"
		p1.axis.major_tick_line_color = None
		p1.axis.minor_tick_line_color = None 
		
		#----padding
		p2.min_border_top = 16
		p1.min_border_bottom = 0
		
		#----axis line
		# p2
		p2.axis.axis_line_width = 0
		p2.axis.axis_line_color = None
		p2.outline_line_color = "#1c1d1e"
		# p1
		p1.background_fill_color = "#dfe1e4"
		p1.axis.axis_line_width = 0
		p1.axis.axis_line_color = None
		p1.outline_line_color = "#1c1d1e"
		
		#----get html
		#get componants
		plot = {'top': p2, 'bottom': p1}
		script, div = components(plot)
		#combine
		div = ''.join(list(div.values()))
		plot = div + script
			
		#----clear cache
		reset_output()

		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plot

	@classmethod
	def onset_diff_plot(cls, config, df, meta, drop, y, clip=None):
		"""Plot onset differences using pandas and seaborn.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		meta : :class:`pandas.DataFrame`
			Metadata for each chart.
		drop : :class:`pandas.DataFrame`
			Participants to be dropped.
		y : :obj:`str`
			Variable of interest.
		clip : :obj:`int`
			Clip value for single subject plot.

		Returns
		-------
		odp
			Bokeh or seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#setting clip
		clip = 200 if clip == None else clip
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		importlib.reload(plt); importlib.reload(sns)
		console('running onset_diff_plot(%s)'%(y), 'blue')
		html_plots = []
		
		#-----------------participant trend line
		individual = meta['individual']
		sns.set(style=config['style']['seaborn'], font_scale=1.25, font="Helvetica")
		#sns.despine(offset=10, trim=True)
		fig, ax = plt.subplots(figsize=(10,5))
		#plot
		df["%s (msec)"%(y)] = df.groupby('participant')[y].rolling(window=5).mean().reset_index(0,drop=True)
		##if diff_dotloc and diff_stim < 500 msec
		sns.color_palette("RdBu",6)
		ax_ = sns.lineplot(x="TrialNum_", y="%s (msec)"%(y), ax=ax, legend=False, lw=1, estimator=None, palette="ch:2.5,.25",
						hue="participant", units="participant", data=df.loc[df['onset_greater'] == False])
		##if diff_dotloc and diff_stim > 500 msec
		ax_ = sns.lineplot(x="TrialNum_", y="%s (msec)"%(y), ax=ax, legend=False, lw=1, estimator=None, alpha=0.2,
						hue="participant", units="participant", data=df.loc[df['onset_greater'] == True])
		#settings
		ax_.set(xlim=(0, 1), ylim=(0, clip), ylabel='| %s (msec) |'%(y), xlabel='trial')
		
		#append
		html_plots.append({"title":individual['title'],"file":"%s.png"%(individual['file']),"footnote":individual['footnote'],"anchor":"itl"})
		#tight layout
		fig.tight_layout()
		#save
		fig.savefig(individual['path'], dpi=300)
		#clear figure from memory
		plt.close(fig)
		
		#-----------------diff plot
		group = meta['group']
		#for each plot
		importlib.reload(plt); importlib.reload(sns)
		fig, ax = plt.subplots(figsize=(10,10), ncols=2, nrows=2, sharey=False)
		ax1, ax2, ax3, ax4 = ax.flatten()
		sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
		#sns.despine(offset=10, trim=True)
		#binned
		##iaps
		type_='iaps'
		df_1 = df.loc[df['trialType'] == type_].reset_index(level=0, drop=True)
		sns.regplot(x="TrialNum_",y=y,color="#5874a6",data=df_1,ax=ax1,
					fit_reg=True,x_bins=group['binned']['bins'],scatter_kws={"s": 40},line_kws={'color': 'red'})
		ax1.set(ylim=(0, 200), xlabel='', ylabel='| %s (msec) |'%(y), title='trial_type=%s'%(type_))
		##pofa
		type_='pofa'
		df_1 = df.loc[df['trialType'] == type_].reset_index(level=0, drop=True)
		sns.regplot(x="TrialNum_",y=y,color="#5d9f6c",data=df_1,ax=ax2,
					fit_reg=True,x_bins=group['binned']['bins'],scatter_kws={"s": 40},line_kws={'color': 'red'})
		ax2.set(ylim=(0, 200), xlabel='', ylabel='', title='trial_type=%s'%(type_))
		#unbinned
		##iaps
		type_='iaps'
		df_1 = df.loc[df['trialType'] == type_].reset_index(level=0, drop=True)
		sns.regplot(x="TrialNum_",y=y,color="#5874a6",data=df_1,ax=ax3,
					fit_reg=True,x_bins=group['unbinned']['bins'],scatter_kws={"s": 5},line_kws={'color': 'red'})
		ax3.set(ylim=(0, 1000), xlabel='trial', ylabel='| %s (msec) |'%(y), title='trial_type=%s'%(type_))
		##pofa
		type_='pofa'
		df_1 = df.loc[df['trialType'] == type_].reset_index(level=0, drop=True)
		sns.regplot(x="TrialNum_",y=y,color="#5d9f6c",data=df_1,ax=ax4,
					fit_reg=True,x_bins=group['unbinned']['bins'],scatter_kws={"s": 5},line_kws={'color': 'red'})
		ax4.set(ylim=(0, 1000), xlabel='trial', ylabel='', title='trial_type=%s'%(type_))
		#append
		html_plots.append({"title":group['title'],"file":"%s.png"%(group['file']),"footnote":group['footnote'],"anchor":"gtl"})
		#tight layout
		fig.tight_layout()
		#save
		fig.savefig(group['path'], dpi=300)
		#clear figure from memory
		plt.close(fig)
		
		#--------finished
		#timestamp
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return html_plots

	@classmethod
	def density_plot(cls, config, df, title):
		"""
		Create density plot (draws kernel density estimate), using seaborn and pandas.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		title : :obj:`str`
			Chart title.

		Returns
		-------
		cm
			Bokeh or seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		#seaborn
		#https://seaborn.pydata.org/generated/seaborn.distplot.html
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)
		console('running density_plot()', 'blue')
		
		#remove cesd_group, trialType_ column
		df = df.drop(['trialType_','cesd_group'], axis=1)
		
		#create metadata to be used for html file
		html_plots = []
		
		#for each column
		for item in (df.columns.tolist()):
			#----plot type
			sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
			sns.despine(offset=10, trim=True)

			#----plot
			fig, ax = plt.subplots(figsize=(10,10))
			density = sns.distplot(df[item], ax=ax, norm_hist=True,
								#bins - number of bins for histogram
								hist=True, bins=20,
								#bw (bandwidth) - width of the kernels
								kde=True, kde_kws={'clip':None,'bw':'scott','kernel':'gau'}).set(xlim=(None, None))
			ax.set(ylabel='Density')
			
			#----design
			#tight layout
			fig.tight_layout()
			
			#----save
			#check if path exists
			file = "density_%s.png"%(item)
			path = config['path']['output'] + "/analysis/html/img/density/"
			if not os.path.exists(path):
				os.makedirs(path)
			#save
			fig.savefig(path + file, dpi=300)
			
			#append 
			html_plots.append({"title":'%s for %s.'%(title, item),"file":'density/'+file,"footnote":""})
			#clear figure from memory
			plt.close(fig)
			
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return density, html_plots

	@classmethod
	def corr_matrix(cls, config, df, path, title, method, footnote=None):
		"""
		Create correlation matrix using bokeh and pandas.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		path : :obj:`str`
			The directory path to save the bokeh or plot.
		method : :obj:`str`
			Spearman or Pearsons correlation coefficient.
		title : :obj:`str`
			Chart title.
		footnote : :obj:`str`
			Chart footnote.

		Returns
		-------
		cm
			Bokeh plot.
		"""
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('running bokeh corr_matrix()', 'blue')

		# math
		import bisect
		from math import pi
		from numpy import arange
		from itertools import chain
		from collections import OrderedDict
		
		# bokeh
		from bokeh.models import ColorBar, LinearColorMapper, TapTool, HoverTool, Range1d, ColumnDataSource
		from bokeh.models.callbacks import CustomJS
		from bokeh.plotting import reset_output, figure
		from bokeh.embed import components

		#create color palette
		matplotlib.use('Agg')
		import matplotlib.colors
		from matplotlib import cm as mpl_cmap
		
		#p-values
		if method == "pearson":
			from scipy.stats import pearsonr as cf
		elif method == "spearman":
			from scipy.stats import spearmanr as cf
		
		#calculate p-values correlation coefficent
		def get_pvalue(df):
			df = df.dropna()._get_numeric_data()
			dfcols = pd.DataFrame(columns=df.columns)
			pvalues = dfcols.transpose().join(dfcols, how='outer')
			for row in df.columns:
				for column in df.columns:
					pvalues[row][column] = round(cf(df[row], df[column])[1], 4)
			return pvalues

		#Gets bounds for quads with n features
		def get_bounds(n):
			bottom = list(chain.from_iterable(
				[[ii]*nlabels for ii in range(nlabels)]))
			top = list(chain.from_iterable(
				[[ii+1]*nlabels for ii in range(nlabels)]))
			left = list(chain.from_iterable(
				[list(range(nlabels)) for ii in range(nlabels)]))
			right = list(chain.from_iterable(
				[list(range(1, nlabels+1)) for ii in range(nlabels)]))
			corr_items = list(chain.from_iterable(
				[[ii+1]*nlabels for ii in range(nlabels)]))
			return top, bottom, left, right, corr_items
		
		#Aligns color values from palette with the correlation coefficient and p-values
		def get_colors_corr(corr_array, p_array, colors):
			c_corr = arange(-1, 1, 1/(len(colors)/2))
			corr_color = []
			p_color = []
			corr_list = []
			p_list = []
			factor_list = []
			itr = 0
			for corr, pvalue in zip(corr_array,p_array):
				ind = bisect.bisect_left(c_corr, corr)
				#colors
				##corr_color
				if (itr)%(nlabels+1)==0:
					corr_color.append('#022f62')
				else:
					corr_color.append(colors[ind-1])
				##pcolor
				if pvalue<=0.05:
					p_color.append("#F44336")
				else:
					p_color.append(colors[ind-1])
				#append corr and pvalues
				corr_list.append(corr)
				p_list.append(pvalue)
				factor_list.append(['[%s, %s]'%(l_labels[itr][0],l_labels[itr][1])])
				itr = itr + 1
			return corr_color, corr_list, p_color, p_list, factor_list
		
		#create seaborn plots for each possible correlation combinations
		def get_corr_plot(df, x, y):
			importlib.reload(plt); importlib.reload(sns)

			#----parameters
			#figure
			fig, ax = plt.subplots(1, 1, figsize=(10,10))
			#bounds
			plt.subplots_adjust(top=0.95, bottom=0.085, left=0.125, right=0.975, hspace=0.2, wspace=0.2)
			
			#----title
			title = "x=%s, y=%s"%(x,y)
			
			#----set style
			#pal = sns.color_palette("Set1", n_colors=2, desat=.5)
			sns.set(style=config['style']['seaborn'], font="Helvetica", font_scale=1.1)
			sns.despine(offset=10, trim=True)
			
			#----plot
			#if grouping #g = sns.lmplot(x, y, hue=groupby, data=df, palette=pal).set_title(title)
			sns.regplot(x, y, data=df, scatter=True, fit_reg=True, ci=95).set_title(title)
			
			#----add rho, p-value to plot
			#get values
			corr_r = cf(df[x],df[y])
			corr_t = "r = %.2f, p < %.2f"%(corr_r[0],corr_r[1])
			#place text 90% of max-x and 90% of max-y
			fig.text(0.85, 0.05, corr_t, size=16, horizontalalignment='center',
					verticalalignment='center', transform=ax.transAxes)

			#----save
			#check if path exists
			file = "%s.png"%(title)
			path = config['path']['output'] + "/analysis/html/img/corr/"
			if not os.path.exists(path):
				os.makedirs(path)
			#save
			fig.savefig(path + file, dpi=300)

		#reset
		reset_output()
		
		#get color map
		RdYlBu = mpl_cmap.get_cmap('RdBu', 32)
		colors_np = np.vstack(RdYlBu(np.linspace(0, 1, 32)))
		cmap = matplotlib.colors.ListedColormap(colors_np, name='RedBlue')
		colors = [matplotlib.colors.rgb2hex(cmap(x/32)[:3]) for x in range(32)]

		# calculate correlation coefficients
		#coeff
		corr_coeff = df.corr(method=method)
		#p-value
		p_value = get_pvalue(df)

		# get list and number of variables
		labels = df.columns
		nlabels = len(corr_coeff)

		#create list of labels
		##x
		l_labels_x = labels.tolist() * nlabels
		##y
		l_labels_y = labels.tolist() * nlabels
		###split into chunks
		l_labels_y_split = np.hsplit(np.array(np.array_split(l_labels_y, nlabels)), nlabels)
		l_labels_y = np.concatenate(l_labels_y_split).ravel().tolist()
		##combine
		l_labels = list(zip(l_labels_x, l_labels_y))

		#create coeff label coordinates
		num = np.arange(0, nlabels, 0.5).tolist()
		num = [x for x in num if x not in np.arange(0, nlabels).tolist()]
		##x
		num_x = [(x - 0.025) for x in num]
		num_x = [x for x in num_x if x not in np.arange(0, nlabels).tolist()] * nlabels
		##y
		num_y = [(x + .125) for x in num]
		num_y = [y for y in num_y if y not in np.arange(0, nlabels).tolist()] * nlabels

		###split into chunks
		num_y_split = np.hsplit(np.array(np.array_split(num_y, nlabels)), nlabels)
		num_y = np.concatenate(num_y_split).ravel().tolist()
		##combine
		#num_labels = list(zip(num_x, num_y))

		#plot
		tools="box_select,save,reset"
		cm = figure(tools=tools, plot_width=1200, plot_height=1200, x_range=(0, nlabels), y_range=(0, nlabels), 
					output_backend="webgl")

		#hover
		hover = HoverTool()
		hover.tooltips = [
			("factor", "@factor"),
			("rho", "@coeff_num"),
			("p", "@p_num"),
		]
		#cm.tools.append(hover)
		cm.hover.line_policy = "nearest"

		#legend
		#cm.toolbar.logo = None

		#grid
		cm.xgrid.grid_line_color = None
		cm.ygrid.grid_line_color = None
		cm.xaxis.major_label_orientation = pi/4
		cm.yaxis.major_label_orientation = pi/4

		#turn off scientific notation
		cm.left[0].formatter.use_scientific = False

		# prepare squares for plot
		top, bottom, left, right, corr_items = get_bounds(nlabels)
		color_list,corr,p_color,pvalues,factor = get_colors_corr(corr_coeff.values.flatten(),p_value.values.flatten(),colors)

		#prepare corr_coeff label for plot
		coeff_color = ["#444444"] * (nlabels * nlabels)
		coeff_color = ['#f9f9f9' if cr >= np.float64(0.74) else cof for cof, cr in zip(coeff_color, corr)]
		coeff_num = ["%.2f"%(x) for x in corr]

		#prepare pvalue for plot
		p_color = ["#444444"] * (nlabels * nlabels)
		p_color = ['#F44336' if p <= np.float64(0.05) else cof for pcol, cof, p in zip(p_color, coeff_color, pvalues)]
		p_num = ["(%.2f)"%(y) for y in pvalues]

		#data source
		source = ColumnDataSource(data=dict(left=left, right=right, top=top, bottom=bottom,
											x=num_x, y=num_y, coeff_num=coeff_num, coeff_color=coeff_color,
											p_num=p_num, factor=factor, square_color=color_list, p_color=p_color))
		#create squares
		cm.quad(top='top', bottom='bottom', left='left', right='right', 
				line_color='white', color='square_color', source=source)

		#create text
		#correlation coefficient
		cm.text(x='x', y='y', source=source, text_color="coeff_color", text='coeff_num', 
				text_font_style='normal', text_font_size = '1.5em', text_align='center',
				y_offset=-5, x_offset=1, text_line_height=1)
		#p-value
		cm.text(x='x', y='y', source=source, text_color="p_color", text='p_num', 
				text_font_style='normal', text_font_size = '1.0em', text_align='center',
				y_offset=20, x_offset=1, text_line_height=1)

		#callback
		link = CustomJS(args = dict(source = source), code="""
			console.log("second")
			obj = cb_obj
			s = source
			data = source.data
			attr = source.attributes
			d = attr.data

			//selected item
			id = source.selected['1d'].indices
			item = d.factor[id][0].replace(/[[\]]/g,'').match(/(".*?")|(\S+)/g)
			console.log('id: ' + id + ', item: ' + item)
			
			//url
			path = window.location.href
			current = path.split("/").slice(0,-1).join("/")
			url = current + '/reg.html?x='+ item[0] + 'y=' + item[1]
			window.open(url)
			//window.open(url, 'newwindow', config='height=720, width=1280')
		""")
		cm.add_tools(TapTool(callback=link))  

		#reverse y-axis
		cm.y_range = Range1d(nlabels, 0)
		#p.y_range = Range1d(monitorSize[1], 0)

		# Set ticks with labels
		ticks = [tick+0.5 for tick in list(range(nlabels))]
		tick_dict = OrderedDict([[tick, labels[ii]] for ii, tick in enumerate(ticks)])

		# Create the correct number of ticks for each axis
		cm.xaxis.ticker = ticks
		cm.yaxis.ticker = ticks

		# Override the labels
		cm.xaxis.major_label_overrides = tick_dict
		cm.yaxis.major_label_overrides = tick_dict

		#color bar
		mapper = LinearColorMapper(
			palette=colors,
			low=-1, high=1
		)
		color_bar = ColorBar(color_mapper=mapper, location=(0, 0))
		cm.add_layout(color_bar, 'right')

		#create link for each comparison
		all_columns = [[x,y] for x in list(labels) for y in list(labels)]
		for idx, clm in enumerate(all_columns):
			get_corr_plot(df=df, x=clm[0], y=clm[1])

		#get html
		script, div = components(cm)
		
		##convert seperate plots and div to single string
		plots = (''.join(map(str, [div, '\n', script])))

		#create html
		html(cls, config, path=path, plots=plots, source='bokeh', title=title, footnote=footnote)

		#reset
		reset_output()

		#--------finished
		#timestamp
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return cm

	@classmethod
	def single_subject(cls, config, df, path):
		"""Create single subject scatterplot using seaborn and pandas.

		Parameters
		----------
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		path : :obj:`str`
			The directory path to save the bokeh or seaborn plot.

		Returns
		-------
		cm
			Bokeh or seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('running single_subject()', 'blue')
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)
		
		#styling plot
		sns.set(style="darkgrid", font_scale=1, font="Helvetica")
		sns.set_context("paper")

		#events
		events = self.config['events']
		
		#get first pofa and iaps
		first = df.drop_duplicates('trialType', keep='first')['TrialNum'].tolist()
		
		#get display size
		xy = [int(i) for i in (df['monitorSize.px'].values[0]).split('x')]
		
		#drop x - outside coordinates
		df = df.loc[~(df['sg_x']<0)] #drop all gaze_x < 0
		df = df.loc[~(df['sg_x']>xy[0])] #drop all gaze_x > width
		
		#drop timestamp - outside coordinates
		df = df.loc[~(df['timestamp']>8000)]
		
		#drop all non fixations
		df = df.loc[~(df['sg_fix_all']==False)]
		
		#loop to draw event markers
		def xline(x, **kwargs):
			data = kwargs.pop("data")
			trialType = data['trialType'].values[0]
			TrialNum = data['TrialNum'].values[0]
			#check if first iaps or first pofa
			if TrialNum in first:
				#fixation onset
				time = events['fixation']
				plt.axvline(x=0, c='red', linewidth=2)
				##annotation
				plt.text(s="Fixation Onset", x=(0 + 100), y=(xy[0] - 25), fontsize=12)
				#fixation offset
				time = events['fixation']
				plt.axvline(x=time, c='red', linewidth=2)
				##annotation
				plt.text(s="Stimulus Offset", x=(time + 100), y=(xy[0] - 25), fontsize=12)
				#stimulus offset
				time = events['fixation'] + events['stimulus'][trialType]
				#line
				plt.axvline(x=time, c='red', linewidth=2)
				#annotation
				plt.text(s="Dotloc Offset", x=(time + 100), y=(xy[0] - 25), fontsize=12)
			
		#plot    
		pal = sns.color_palette("RdBu", n_colors=192)    
		sp = sns.FacetGrid(col="trialType", data=df, height=5, palette=pal, hue="TrialNum")
		sp.map(plt.scatter, "timestamp", "sg_x", alpha=0.3, s=10)
		sp.map_dataframe(xline, x="timestamp")
				
		#post
		ax = sp.axes.flatten()
		ax[0].set_ylabel("Gaze Coordinates (x)", fontdict={'fontsize':12})
		ax[0].set_title("trialType = IAPS", fontdict={'fontsize':12})
		ax[0].tick_params(labelsize=12)
		ax[1].set_title("trialType = POFA", fontdict={'fontsize':12})
		ax[1].tick_params(labelsize=12)
		
		#size
		sp.fig.set_size_inches(20,10)
		
		#save
		sp.savefig(path, dpi=300)
		
		#clear figure from memory
		plt.close()
		
		#--------finished
		#timestamp
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plt 

	@classmethod
	def boxplot(cls, config, df, path=None, x=None, y=None, title=None, plots=None, cat='analysis'):
		"""
		Creates boxplot using seaborn and pandas.
		
		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		path : :obj:`str`
			Path to save data.
		drift : :obj:`str`
			X-axis.
		drift : :obj:`str`
			Y-axis.
		title : :obj:`str`
			Plot title.
		plots : :obj:`dict`
			Dictionary of plots metadata.
		cat : :obj:`str`
			Type of plot.
		""" 
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('running boxplot(%s)'%(y), 'blue')

		# start
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		importlib.reload(plt); importlib.reload(sns)
		sns.set(style=config['style']['seaborn'], font_scale=1.5, font="Helvetica")
		sns.despine(offset=10, trim=True)
		short_ = config['metadata']['short']
		#if looking at dwell time
		if cat=='bias':
			#plot
			fig, (ax1, ax2) = plt.subplots(figsize=(10,10),ncols=2,sharey=True)
			
			#design
			##for each unique value in x-axis variable, assign color
			cat_x = df[x].unique().tolist()
			color=['b','r','g','y','o']
			pal = dict(zip(cat_x, color))
			
			#create plot
			for ax, trialType in zip([ax1,ax2],['iaps','pofa']):
				df_ = df.loc[df['trialType'].isin([trialType])]
				sns.boxplot(x=x, y='.fitted', data=df_, ax=ax, palette=pal)
				sns.swarmplot(x=x, y='.fitted', data=df_, color=".25", ax=ax)
				ax.set_title(trialType)
				ax.set_xlabel(short_[x])
				if trialType == 'pofa':
					ax.set_ylabel('')
		
		#if looking at dwell time
		if cat=='dwell':
			#plot
			fig, (ax1, ax2) = plt.subplots(figsize=(10,10),ncols=2,sharey=True)
			
			#design
			##for each unique value in x-axis variable, assign color
			cat_x = df[x].unique().tolist()
			color=['b','r','g','y','o']
			pal = dict(zip(cat_x, color))
			
			#create plot
			for ax, trialType in zip([ax1,ax2],['iaps','pofa']):
				df_ = df.loc[df['trialType'].isin([trialType])]
				sns.boxplot(x=x, y='.fitted', data=df_, ax=ax, palette=pal)
				sns.swarmplot(x=x, y='.fitted', data=df_, color=".25", ax=ax)
				ax.set_title(trialType)
				ax.set_xlabel(short_['aoi'])
				if trialType == 'pofa':
					ax.set_ylabel('')
		
		#if looking at variables like gaze_bias, dp_bias
		elif cat=='analysis':
			#plot
			fig, (ax1, ax2, ax3, ax4) = plt.subplots(figsize=(19.2, 10.8),ncols=4,sharey=False)
			
			#design
			##for each unique value in x-axis variable, assign color
			cat_x = df[x].unique().tolist()
			color=['b','r','g','y','o']
			pal = dict(zip(cat_x, color))
			
			#create plot
			for ax, itm in zip([ax1,ax2,ax3,ax4],y):
				#dp_bias
				sns.boxplot(x=x, y=itm, data=df, ax=ax, palette=pal)
				sns.swarmplot(x=x, y=itm, data=df, color=".25", ax=ax)
				ax.set(xlabel=x)
				
		#if looking at variables like race, gender, os        
		elif cat=='demographics':
			#layout
			sns.set(style=config['style']['seaborn'], palette="deep", font_scale=1.1, font="Helvetica")
			sns.despine(offset=10, trim=True)
			fig, (ax1,ax2,ax3,ax4) = plt.subplots(figsize=(20, 10),ncols=4,sharey=True)
			palette = sns.color_palette(["#e74c3c", "#3f51b5", "#2ecc71","#9b59b6","#ffb400"])
			#counter
			counter = 0
			for ax, x_, in zip([ax1,ax2,ax3,ax4],x):
				##exclude smaller race categories for display purposees
				if x_=='gender':
					df_ = df.loc[~df['gender'].isin(["other"])].drop_duplicates(subset="participant",keep="first")
				##exclude smaller race categories for display purposes
				elif x_=='race':
					df_ = df.loc[~df['race'].isin(["American Indian or Alaska Native","Two or more races",
					"Black or African American","None of the above"])].drop_duplicates(subset="participant",keep="first")
				else:
					df_ = df.drop_duplicates(subset="participant",keep="first")
				#plot
				sns.boxplot(x=x_, y=y, palette=palette, ax=ax, data = df_)
				#remove label
				if counter!=0:
					ax.set_ylabel('')
				#counter
				counter = counter + 1
		
			#layer
			sns.despine(offset=10, trim=True)
		
		#tight layout
		fig.tight_layout()
		
		##save
		fig.savefig(path, dpi=300)
		
		#clear from memory
		plt.close(fig) 
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')

	@classmethod
	def cooks_plot(cls, config, y, model, path, df):
		"""Create cooks distance plot plot using seaborn, pandas, and statsmodel.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		path : :obj:`str`
			The directory path save the seaborn plot.
		y : :obj:`str`
			The predictor variable.
		model : :obj:`dict`
			statsmodel model.

		Returns
		-------
		res
			seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		console('running cooks_plot(%s)'%(y), 'blue')
		#-------plot
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)

		#plot design
		sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
		sns.despine(offset=10, trim=True)

		#plot
		fig, ax = plt.subplots(figsize=(10,10))
		
		# influence
		influence = model.get_influence()
		
		# cook's distance, from statsmodels internals
		## c is the distance and p is p-value
		(c, p) = influence.cooks_distance
		
		#plot
		plt.stem(np.arange(len(c)), c, markerfmt=",")
		plt.xlabel('Observation Number')
		plt.ylabel('Cooks Distance')
		
		# annotations
		particpants = list(df['participant'])
		leverage_lines = np.flip(np.argsort(c), 0)[:3]
		for i in leverage_lines:
			plt.annotate(particpants[i], xy=(i, c[i]))

		#tight layout
		fig.tight_layout()
		
		#save
		fig.savefig(path, dpi=300)
		
		#clear figure from memory
		plt.close(fig)
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plt
	
	@classmethod
	def residual_plot(cls, config, y, residuals, path):
		"""Create probability plot using seaborn, pandas, and rpy2.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		y : :obj:`str`
			Predictor variable.
		residuals : :class:`pandas.DataFrame`
			Pandas dataframe of residuals vs fitted, qq data, and raw data.
		path : :obj:`str`
			The directory path save the seaborn plot.

		Returns
		-------
		lmp
			seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		
		console('running residual_plot(%s)'%(y), 'blue')
		#-------plot
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)
		
		#-------plot design
		sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
		sns.despine(offset=10, trim=True)
		#-------plot
		fig, ax = plt.subplots(figsize=(10,10))
		#residual vs fitted plot
		cmap=sns.color_palette(["#bd282f","#226db2"])
		sns.scatterplot(x=".fitted", y=".resid", data=residuals, ax=ax, hue="trialType", palette=cmap, legend=None, s=60, edgecolor=None)
		#loess smoothing curve
		sns.regplot(x=".fitted", y=".resid", data=residuals, ax=ax, lowess=True, scatter=False, color="blue")
		#horizontal line
		plt.axhline(0, zorder=1, color='black', linewidth=2.25)
		#annotations
		##max
		max_ = residuals[residuals['.resid']==residuals['.resid'].max()]
		max_x = max_['.fitted'].iloc[0]
		max_y = max_['.resid'].iloc[0]
		max_subject = max_['participant'].iloc[0]
		plt.text(max_x, max_y, max_subject, color='black', weight='semibold')
		##min
		min_ = residuals[residuals['.resid']==residuals['.resid'].min()]
		min_x = min_['.fitted'].iloc[0]
		min_y = min_['.resid'].iloc[0]
		min_subject = min_['participant'].iloc[0]
		plt.text(min_x, min_y, min_subject, color='black', weight='semibold')

		#-------plot design
		#label
		plt.xlabel('.fitted')
		plt.ylabel('.resid')
		#tight layout
		fig.tight_layout()
		
		#-------save
		fig.savefig(path, dpi=300)
		
		#-------clear figure from memory
		plt.close(fig)
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plt

	@classmethod
	def qq_plot(cls, config, y, residuals, path):
		"""Create probability plot using seaborn, pandas, and rpy2.

		Parameters
		----------
		config : :obj:`dict`
			Configuration data.
		y : :obj:`str`
			Predictor variable.
		residuals : :class:`pandas.DataFrame`
			Pandas dataframe of residuals vs fitted, qq data, and raw data.
		path : :obj:`str`
			The directory path save the seaborn plot.

		Returns
		-------
		lmp
			seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('running qq_plot(%s)'%(y), 'blue')
		#-------plot
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)

		#plot design
		sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
		sns.despine(offset=10, trim=True)
		#plot
		fig, ax = plt.subplots(figsize=(10,10))
		#qq residuals
		cmap=sns.color_palette(["#bd282f","#226db2"])
		sns.scatterplot(x="theoretical", y=".resid", data=residuals, ax=ax, hue="trialType", palette=cmap, legend=None, s=60, edgecolor=None)
		sns.regplot(x="theoretical", y=".resid", data=residuals, ax=ax, scatter=None, color="black", ci=None)
		#label
		plt.xlabel('Theoretical Quantiles')
		plt.ylabel('Sample Quantiles')
		#annotations
		##max
		max_ = residuals[residuals['.resid']==residuals['.resid'].max()]
		max_x = max_['theoretical'].iloc[0]
		max_y = max_['.resid'].iloc[0]
		max_subject = max_['participant'].iloc[0]
		plt.text(max_x, max_y, max_subject, color='black', weight='semibold')
		##min
		min_ = residuals[residuals['.resid']==residuals['.resid'].min()]
		min_x = min_['theoretical'].iloc[0]
		min_y = min_['.resid'].iloc[0]
		min_subject = min_['participant'].iloc[0]
		plt.text(min_x, min_y, min_subject, color='black', weight='semibold')
		
		#tight layout
		fig.tight_layout()
		
		#save
		fig.savefig(path, dpi=300)
		
		#clear figure from memory
		plt.close(fig)
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return plt

	@classmethod
	def logit_plot(cls, config, df, path, param):
		"""Create logistic regression plot using seaborn and pandas

		Parameters
		----------
		df : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		path : :obj:`str`
			The directory path save the seaborn plot.
		param : :obj:`dict`
			x, y, groupby parameters.

		Returns
		-------
		lmp
			seaborn plot.
		"""
		#----initiate fonts
		__font__()
		
		#----for timestamp
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('processing.logit_plot()', 'blue')
		matplotlib.use('Agg')
		import matplotlib.pyplot as plt
		import seaborn as sns
		importlib.reload(plt); importlib.reload(sns)
		
		#plot design
		p=param
		sns.set(style=config['style']['seaborn'], font_scale=1.1, font="Helvetica")
		sns.despine(offset=10, trim=True)
		console('running logit_plot(), (y=%s, x=%s)'%(p['y'],p['x']), 'blue')

		#plot
		fig, (ax1,ax2) = plt.subplots(figsize=(19.2, 10.8), ncols=2, sharey=True)
		#iaps
		type_ = 'iaps'
		df_ = df.loc[(df['trialType_']==type_)]
		lmp = sns.regplot(x=p['x'], y=p['y'], data=df_, logistic=True, y_jitter=.02, ax=ax1, color="#5874a6")
		ax1.set(xlabel='trial type', title='trialType=%s'%(type_))
		#pofa
		type_ = 'pofa'
		df_ = df.loc[(df['trialType_']==type_)]
		lmp = sns.regplot(x=p['x'], y=p['y'], data=df_, logistic=True, y_jitter=.02, ax=ax2, color="#5d9f6c")
		ax2.set(xlabel='trial type', title='trialType=%s'%(type_))

		#tight layout
		fig.tight_layout()
		
		#save
		fig.savefig(path, dpi=300)
		
		#clear figure from memory
		plt.close(fig)
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return lmp

	@classmethod
	def html(cls, destination=None, df=None, raw_data=None, name=None, path=None, plots=None, source=None, title=None, intro=None, footnote=None, script="", **kwargs):
		"""
		Create HTML output.

		Parameters
		----------
		destination : :obj:`str`
			Path to save file to.
		df : :class:`pandas.DataFrame`
			Pandas dataframe of analysis results data.
		raw_data : :class:`pandas.DataFrame`
			Pandas dataframe of raw data.
		name : :obj:`str`
			(py::`if source is logit`) The name of csv file created.
		path : :obj:`str`
			The directory path of the html file.
		plots : :obj:`dict`
			If generating seaborn images, the list of plots used.
		source : :obj:`str`
			The type of data being recieved.
		trial : :obj:`str`
			(If Bokeh) Trial Number.
		session : :obj:`str`
			(If Bokeh) Session Number.
		bokeh_type : :obj:`str`
			(If Bokeh) Control directory location. If trial, create trial plots.
		title : :obj:`str`
			The title of the table or figure.
		intro : :obj:`str`
			The introduction of the group of figures or tables.
		footnote : :obj:`str`
			The footnote of the table or figure.
		metadata : :obj:`dict`
			Additional data to be included.
		**kwargs : :obj:`str`, :obj:`int`, or :obj:`None`, optional
			Additional properties, relevent for specific content types. Here's a list of available properties:
				
			.. list-table::
			:class: kwargs
			:widths: 25 50
			:header-rows: 1
			
			* - Property
				- Description
			* - **short**, **long** : :obj:`str`
				- Short (aoi) and long form (Area of Interest) label of html page. This is primarily used for constructing metadata tags in html.
			* - **display** : :obj:`str`
				- (For bokeh) The type of calibration/validation display.
			* - **trial** : :obj:`str`
				- (For bokeh) The trial number for the eyetracking task.
			* - **session** : :obj:`int`
				- (For bokeh) The session number for the eyetracking task.
			* - **day** : :obj:`str`
				- (For bokeh) The day the eyetracking task was run.

		Returns
		-------
		html : :obj:`str`
			String of html code.
		"""
		#----initiate fonts
		cls.__font__()
		
		#timestamp
		date = datetime.datetime.now().replace(microsecond=0).strftime('%Y-%m-%d %H:%M:%S')
		_t0 = datetime.datetime.now()
		_f = debug(message='t', source="timestamp")
		console('running plot.html()', 'blue')
		
		#---copy csv, imhr, js files to each model
		#copy files
		for folder in ['css','imhr','js']:
			import imhr
			p_ = str(Path(imhr.__file__).parent.parent)
			s_ = '%s/docs/source/_templates/html/%s/'%(p_, folder)
			d_ = '%s/%s/'%(os.path.dirname(path), folder)
			#shutil.move(s_, d_)
			import distutils
			distutils.dir_util.copy_tree(s_, d_)
		
		#----if creating html file from bokeh
		if ((source=='bokeh') or (source=='methods')):
			#get short and long names
			short = kwargs['short'] if 'short' in kwargs else source
			long = kwargs['long'] if 'long' in kwargs else source
			
			#if trial plot
			if (source=='bokeh'):
				display =  kwargs['display'] if 'display' in kwargs else None
				trial =  kwargs['trial'] if 'trial' in kwargs else None
				session =  kwargs['session'] if 'session' in kwargs else None
				day =  kwargs['day'] if 'day' in kwargs else None
				if ((display=='trial') or (display=='calibration') or (display=='validation')):
					parent='../'
				else:
					parent=''
			else:
				parent=''
				
			#build header
			link = []; css = []; js = []
			link.append('<title>IMHR [webgazer] %s</title>'%(long))
			link.append('<meta name="title" content="mdl[webgazer]: %s">'%(long))
			link.append('<meta name="description" content="Results from our feasibility study for in-browser eyetracking.">')
			link.append('<meta name="author" content="Semeon Risom">')
			link.append('<meta name="email" content="semeon.risom@gmail.com">')
			link.append('<meta name="robots" content="index,follow">')
			link.append('<meta name="AdsBot-Google" content="noindex">')
			link.append('<!--size-->')
			link.append('<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8"/>')
			link.append('<!--no cache-->')
			link.append('<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>')
			link.append('<meta http-equiv="Pragma" content="no-cache"/>')
			link.append('<meta http-equiv="Expires" content="0"/>')
			link.append('<!--icons-->')
			link.append('<link rel="shortcut icon" href="'+parent+'imhr/favicon/favicon.ico">')
			link.append('<link rel="icon" sizes="16x16 32x32 64x64" href="'+parent+'imhr/favicon/favicon.ico">')
			link.append('<link rel="icon" type="image/png" sizes="196x196" href="'+parent+'imhr/favicon/favicon-192.png">')
			link.append('<link rel="icon" type="image/png" sizes="160x160" href="'+parent+'imhr/favicon/favicon-160.png">')
			link.append('<link rel="icon" type="image/png" sizes="96x96" href="'+parent+'imhr/favicon/favicon-96.png">')
			link.append('<link rel="icon" type="image/png" sizes="64x64" href="'+parent+'imhr/favicon/favicon-64.png">')
			link.append('<link rel="icon" type="image/png" sizes="32x32" href="'+parent+'imhr/favicon/favicon-32.png">')
			link.append('<link rel="icon" type="image/png" sizes="16x16" href="'+parent+'imhr/favicon/favicon-16.png">')
			link.append('<link rel="apple-touch-icon" href="'+parent+'imhr/favicon/favicon-57.png">')
			link.append('<link rel="apple-touch-icon" sizes="114x114" href="'+parent+'imhr/favicon/favicon-114.png">')
			link.append('<link rel="apple-touch-icon" sizes="72x72" href="'+parent+'imhr/favicon/favicon-72.png">')
			link.append('<link rel="apple-touch-icon" sizes="144x144" href="'+parent+'imhr/favicon/favicon-144.png">')
			link.append('<link rel="apple-touch-icon" sizes="60x60" href="'+parent+'imhr/favicon/favicon-60.png">')
			link.append('<link rel="apple-touch-icon" sizes="120x120" href="'+parent+'imhr/favicon/favicon-120.png">')
			link.append('<link rel="apple-touch-icon" sizes="76x76" href="'+parent+'imhr/favicon/favicon-76.png">')
			link.append('<link rel="apple-touch-icon" sizes="152x152" href="'+parent+'imhr/favicon/favicon-152.png">')
			link.append('<link rel="apple-touch-icon" sizes="180x180" href="'+parent+'imhr/favicon/favicon-180.png">')
			link.append('<meta name="msapplication-TileColor" content="#FFFFFF">')
			link.append('<meta name="msapplication-TileImage" content="'+parent+'imhr/favicon/favicon-144.png">')
			link.append('<meta name="msapplication-config" content="'+parent+'docs/favicon/browserconfig.xml">')
			css.append('<link rel="stylesheet" type="text/css" href="'+parent+'css/user.css">')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/jquery/jquery-3.2.1.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/user.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/lodash.min.js"></script>')
			css.append('<!--bootstrap-->')
			css.append('<link rel="stylesheet" type="text/css" href="'+parent+'css/bootstrap/bootstrap.css">')
			css.append('<!--bokeh-->')
			css.append('<link rel="stylesheet" type="text/css" href="'+parent+'css/bokeh/bokeh-1.0.4.min.css">')
			css.append('<link rel="stylesheet" type="text/css" href="'+parent+'css/bokeh/bokeh-widgets-1.0.4.min.css">')
			css.append('<link rel="stylesheet" type="text/css" href="'+parent+'css/bokeh/bokeh-tables-1.0.4.min.css">')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bokeh/bokeh-1.0.4.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bokeh/bokeh-widgets-1.0.4.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bokeh/bokeh-tables-1.0.4.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bokeh/bokeh-gl-1.0.4.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bokeh/bokeh-api-1.0.4.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/popper.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="'+parent+'js/bootstrap/bootstrap.js"></script>')
			
			# finish header
			head = ('\n\t'.join(map(str, link))) + ('\n\t'.join(map(str, css))) + ('\n\t'.join(map(str, js)))
			
			#body, footer
			if source=='bokeh':
				#build body
				body = ['<div class="container" style="">',
							'<div class="container bokeh" style="">',
								'<div class="figure %s" trial-id="%s" session-id="%s" day-id="%s" type-id="%s">\
								'%(display, trial, session, day, display),
									'<div class="title"><b>Figure 1.</b> <a>%s</a></div>'%(title),
									'<div class="date"> Last Updated: ' + '%s'%(date) + '</div>',
									plots,
									'<div class="footnote">%s</div>'%(footnote),
								'</div>',
							'</div>',
						'</div>']
				# finish body
				body = ('\n\t'.join(map(str, body)))
				
				#build footer
				if ((display=='trial') or (display=='calibration') or (display=='validation')):
					foot = ['<script>',
								'$(document).ready(function() {getinput();});',
							'</script>']
					# finish footer
					foot = ('\n\t'.join(map(str, foot)))
				else:
					foot = ''
					
			elif source=='methods':
				#build body
				body = ['<div class="container" style="">',
							'<div class="dashboard-main">',
								'<div class="table-container">',
									'<div class="title"><a><b>%s</b></a></div>'%(title) ,
									plots,
								'</div>',
							'</div>',
						'</div>']
				# finish body
				body = ('\n\t'.join(map(str, body)))
				#build footer
				foot = ''
			
			#build html
			html = ['<html short="%s" long="%s">'%(short, long),
						'<head>',
							head,
						'</head>',
						'<body id="body" class="%s" style="margin: 0px; background-color: #434e54 !important;">'%(display),
							body,
						'</body>',
						'<footer>',
							foot,
						'</footer>',
					'</html>']
			# finish html
			html = ('\n'.join(map(str, html)))
			
		#all other html
		else:
			#get short and long names
			short = kwargs['short'] if 'short' in kwargs else source
			long = kwargs['long'] if 'long' in kwargs else source
			var = kwargs['var'] if 'var' in kwargs else source
			
			#build header
			link = []; css = []; js = []
			link.append('<title>IMHR [webgazer] %s</title>'%(long))
			link.append('<meta name="title" content="mdl[webgazer]: %s">'%(long))
			link.append('<meta name="description" content="Results from our feasibility study for in-browser eyetracking.">')
			link.append('<meta name="author" content="Semeon Risom">')
			link.append('<meta name="email" content="semeon.risom@gmail.com">')
			link.append('<meta name="robots" content="index,follow">')
			link.append('<meta name="AdsBot-Google" content="noindex">')
			link.append('<!--size-->')
			link.append('<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8"/>')
			link.append('<!--no cache-->')
			link.append('<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>')
			link.append('<meta http-equiv="Pragma" content="no-cache"/>')
			link.append('<meta http-equiv="Expires" content="0"/>')
			link.append('<meta name="msapplication-TileColor" content="#FFFFFF">')
			link.append('<meta name="msapplication-TileImage" content="imhr/favicon/favicon-144.png">')
			link.append('<meta name="msapplication-config" content="docs/favicon/browserconfig.xml">')
			link.append('<!--icons-->')
			link.append('<link rel="shortcut icon" href="imhr/favicon/favicon.ico">')
			link.append('<link rel="icon" sizes="16x16 32x32 64x64" href="imhr/favicon/favicon.ico">')
			link.append('<link rel="icon" type="image/png" sizes="196x196" href="imhr/favicon/favicon-192.png">')
			link.append('<link rel="icon" type="image/png" sizes="160x160" href="imhr/favicon/favicon-160.png">')
			link.append('<link rel="icon" type="image/png" sizes="96x96" href="imhr/favicon/favicon-96.png">')
			link.append('<link rel="icon" type="image/png" sizes="64x64" href="imhr/favicon/favicon-64.png">')
			link.append('<link rel="icon" type="image/png" sizes="32x32" href="imhr/favicon/favicon-32.png">')
			link.append('<link rel="icon" type="image/png" sizes="16x16" href="imhr/favicon/favicon-16.png">')
			link.append('<link rel="apple-touch-icon" href="imhr/favicon/favicon-57.png">')
			link.append('<link rel="apple-touch-icon" sizes="114x114" href="imhr/favicon/favicon-114.png">')
			link.append('<link rel="apple-touch-icon" sizes="72x72" href="imhr/favicon/favicon-72.png">')
			link.append('<link rel="apple-touch-icon" sizes="144x144" href="imhr/favicon/favicon-144.png">')
			link.append('<link rel="apple-touch-icon" sizes="60x60" href="imhr/favicon/favicon-60.png">')
			link.append('<link rel="apple-touch-icon" sizes="120x120" href="imhr/favicon/favicon-120.png">')
			link.append('<link rel="apple-touch-icon" sizes="76x76" href="imhr/favicon/favicon-76.png">')
			link.append('<link rel="apple-touch-icon" sizes="152x152" href="imhr/favicon/favicon-152.png">')
			link.append('<link rel="apple-touch-icon" sizes="180x180" href="imhr/favicon/favicon-180.png">')
			js.append('<!--all-->')
			js.append('<script type="text/javascript" language="javascript" src="js/jquery/jquery-3.2.1.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/lodash.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/bootstrap/bootstrap.bundle.js"></script>')
			link.append('<!--dataTables-->')
			css.append('<link rel="stylesheet" type="text/css" href="css/bootstrap/bootstrap.min.css">')
			css.append('<link rel="stylesheet" type="text/css" href="css/datatables/dataTables.bootstrap.min.css">')
			css.append('<link rel="stylesheet" type="text/css" href="css/datatables/responsive.bootstrap.min.css">')
			css.append('<link rel="stylesheet" type="text/css" href="css/datatables/buttons.bootstrap.min.css">')
			js.append('<!--user-->')
			link.append('<link rel="stylesheet" type="text/css" href="css/user.css">')
			js.append('<!--dataTables-->')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/jquery.dataTables.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/dataTables.rowsGroup.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/dataTables.rowGroup.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/dataTables.bootstrap.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/dataTables.buttons.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/buttons.bootstrap.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/dataTables.responsive.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/responsive.bootstrap.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/buttons.html5.min.js"></script>')
			js.append('<script type="text/javascript" language="javascript" src="js/datatables/jszip.min.js"></script>')
			js.append('<!--user-->')
			js.append('<script type="text/javascript" language="javascript" src="js/user.js"></script>')
			js.append('<script>source="%s"</script>'%(source))
			# additional libraries
			if ((source == "logit") or (source == "onset") or (source == "anova")):
				link.append('<!--prism-->')
				css.append('<link rel="stylesheet" type="text/css" href="css/prism/prism.css">')
				css.append('<link rel="stylesheet" type="text/css" href="css/prism/prism-line-numbers.css">')
				css.append('<script type="text/javascript" language="javascript" src="js/prism/prism.js"></script>')
				css.append('<script type="text/javascript" language="javascript" src="js/prism/prism-r.js"></script>')
				css.append('<script type="text/javascript" language="javascript" src="js/prism/prism-python.js"></script>')
				css.append('<script type="text/javascript" language="javascript" src="js/prism/prism-line-numbers.js"></script>')
				css.append('<script type="text/javascript" language="javascript" src="js/prism/prism-normalize-whitespace.js"></script>')
			
			# finish header
			head = ('\n\t'.join(map(str, link))) + ('\n\t'.join(map(str, css))) + ('\n\t'.join(map(str, js)))
			
			#if displaying seaborn plots
			if source == "plots":
				html_plots = []
				#for each plot
				for idx, p in enumerate(plots):
					html_p = ['<div class="figure" style="max-width:800px;">',
								#title
								'<div class="title"><b>Figure %s.</b> %s</div>'%((idx+1), p['title']),
								#date
								'<div class="date"> Last Updated: ' + '%s'%(date) + '</div>',
								#figure
								'<a href="img\\' + p['file'] + '">\n<img class="figure" src="img\\' + p['file'] + '">\n</a>',
								#footnote
								'<div class="footnote">%s</div>'%(p['footnote']),
							'</div>']
					html_plots.append(('\n\t'.join(map(str, html_p))))
					
				#build body
				##add none as class only if there is no intro
				if intro!=None:
					intro_class = ''
				else:
					intro_class = None
					
				body = ['<div class="container" style="">',
							'<div class="container-figure %s" style="">'%(display),
								'<div class="intro %s">%s</div>'%(intro_class, intro),
									(''.join(map(str, html_plots))),
								'</div>',
							'</div>',
						'</div>']   
				body = ('\n\t'.join(map(str, body)))
				
				#build html
				html = ['<html>',
						'<head>',
							head,
						'</head>',
						'<body id="body">',
							body,
						'</body>',
						'</html>']
				html = ('\n' .join(map(str, html)))
				
				#save
				with open(path,'w') as html_:
					html_.write(html)
					
				#--------finished
				console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
				return html
			
			#if displaying dataframe tables
			else:         
				#build body
				##save table to html for datatable
				if ((source == "demographic") or (source == "device") or (source == "task") or (source == "summary")):
					#allow word wrapping
					if(source == "demographic") or (source == "task"):
						wrap = "wrap"
					else:
						wrap = "nowrap"
					# save table to csv for downloading
					## check if paths exist
					_folder = '%s/csv'%(Path(path).parent)
					p_ = '%s/%s.csv'%(_folder, name)
					if not os.path.exists(_folder):
						os.makedirs(_folder)
					df.to_csv(p_, index=False)
					#get table
					table = df.to_html(index=False, index_names=True).replace('<table border="1" class="dataframe">',
					'<table id="table" class="table '+source+' table-striped table-bordered \
					hover dt-responsive '+wrap+'" cellspacing="0" width="100%">').replace('&gt;','>').replace('&lt;','<')
				elif (source == "definitions"):
					#wordwrap
					wrap = "nowrap"
					#prevent trucating strings
					width = pd.get_option('display.max_colwidth')
					pd.set_option('display.max_colwidth', -1)
					##save table to csv for downloading
					_folder = '%s/csv'%(Path(path).parent)
					p_ = '%s/%s.csv'%(_folder, name)
					if not os.path.exists(_folder):
						os.makedirs(_folder)
					df.to_csv(p_, index=False)
					#get table
					table = df.to_html(index=False, index_names=True).replace('<table border="1" class="dataframe">',
					'<table id="table" class="table '+source+' table-striped table-bordered \
					hover dt-responsive '+wrap+'" cellspacing="0" width="100%">')   
					#reset
					pd.set_option('display.max_colwidth', width)
				elif ((source == "logit") or (source == "onset") or (source == "anova")):
					#prevent trucating strings
					width = pd.get_option('display.max_colwidth')
					pd.set_option('display.max_colwidth', -1)
					##save table to csv for downloading #here saving raw data for future analysis
					_folder = '%s/csv'%(Path(path).parent)
					p_ = '%s/%s.csv'%(_folder, name)
					if not os.path.exists(_folder):
						os.makedirs(_folder)
					raw_data.to_csv(p_, index=False)
					#get table
					table = df.to_html(index=True, index_names=True).replace('<table border="1" class="dataframe">',
					'<table id="table" data-file="'+ name +'" class="table '+source+' table-striped table-bordered \
					hover dt-responsive nowrap" cellspacing="0" width="100%">')
					#reset
					pd.set_option('display.max_colwidth', width)
				elif (source == "full_summary"):
					#wordwrap
					wrap = "nowrap"
					#prevent trucating strings
					width = pd.get_option('display.max_colwidth')
					pd.set_option('display.max_colwidth', -1)
					##save table to csv for downloading
					_folder = '%s/csv'%(Path(path).parent)
					p_ = '%s/%s.csv'%(_folder, name)
					if not os.path.exists(_folder):
						os.makedirs(_folder)
					df.to_csv(p_, index=False)
					#get table
					table = df.to_html(index=False, index_names=True).replace('<table border="1" class="dataframe">',
					'<table id="table" class="table '+source+' table-striped table-bordered \
					hover dt-responsive '+wrap+'" cellspacing="0" width="100%">')   
					#reset
					pd.set_option('display.max_colwidth', width)
				else:
					##check if paths exist
					_folder = '%s/csv'%(Path(path).parent)
					p_ = '%s/%s.csv'%(_folder, name)
					if not os.path.exists(_folder):
						os.makedirs(_folder)
					# save table to csv for downloading
					df.to_csv(p_ + '%s.csv'%(name), index=False)
					#get table
					table = df.to_html(index=True, index_names=True).replace('<table border="1" class="dataframe">',
					'<table id="table" data-file="'+ name +'" class="table '+source+' table-striped table-bordered \
					hover dt-responsive nowrap" cellspacing="0" width="100%">')
		
				#add images to bottom of model
				if ((source == "logit") or (source == "onset") or (source == "anova")):
					html_plots = []
					#for each plot
					if plots != None:
						fig_num = 0
						for idx, p in enumerate(plots):
							if (source == "anova"):
								#if supplimentary table
								if (p['type'] == 'table'):
									if 'anchor' in p:
										anchor = p['anchor']
									_table = p['df']
									#prevent trucating strings
									width = pd.get_option('display.max_colwidth')
									pd.set_option('display.max_colwidth', -1)
									#convert df to html table
									_table = _table.to_html(index=True, index_names=True).replace('<table border="1" class="dataframe">',
									'<table id="table2" data-file="'+ name +'" class="table '+source+' table-striped table-bordered \
									hover dt-responsive nowrap"\ cellspacing="0" width="100%">')
									#create table
									html_p = [
										'<div class="figure %s" id="%s">'%(source, anchor),
											'<div class="title">%s</div>'%(p['title']),
											'<div class="date"> Last Updated: ' + '%s'%(date) + '</div>',
											'<div class="dataTables_wrapper form-inline dt-bootstrap">',
												'<!--script-->' + '\n',
												_table,
											'</div>',
											'<div class="footnote">%s</div>'%(p['footnote']),
										'</div>']
									html_p = ('\n\t'.join(map(str, html_p)))
									#reset
									pd.set_option('display.max_colwidth', width)
								#else is plot
								else:
									fig_num = fig_num + 1
									if 'anchor' in p:
										anchor = p['anchor']  
									else: 
										anchor = ""
									html_p = ['<div class="figure %s" id="%s">'%(source, anchor),
												#title
												'<div class="title"><b>Figure %d.</b> %s</div>'%(fig_num, p['title']),
												#date
												'<div class="date"> Last Updated: ' + '%s'%(date) + '</div>',
												#figure
												'<a class="img" href="img\\'+p['file']+'">\n<img class="img" src="img\\'+p['file']+'">\n</a>',
												#footnote
												'<div class="footnote">%s</div>'%(p['footnote']),
											'</div>']
							#else is plot
							else:
								fig_num = fig_num + 1
								if 'anchor' in p:
									anchor = p['anchor']  
								else: 
									anchor = ""
								html_p = ['<div class="figure %s" id="%s">'%(source, anchor),
											#title
											'<div class="title"><b>Figure %d.</b> %s</div>'%(fig_num, p['title']),
											#date
											'<div class="date"> Last Updated: ' + '%s'%(date) + '</div>',
											#figure
											'<a class="img" href="img\\'+p['file']+'">\n<img class="img" src="img\\'+p['file']+'">\n</a>',
											#footnote
											'<div class="footnote">%s</div>'%(p['footnote']),
										'</div>']
							
							#----append to html plots 
							html_plots.append(('\n\t'.join(map(str, html_p))))
						
					html_plots = '\n\t'.join(map(str, html_plots))
				else:
					html_plots = ""
					
				##body
				body = ['<div class="container-large %s" style="">'%(source),
							'<div class="container" style="">',
								'<div class="dashboard-main">',
									'<div class="table-container %s">'%(source),
										'<div class="title">'+'%s'%(title)+'</div>',
										'<div class="date"> Last Updated: '+'%s'%(date),
											'<span class="link" id="code">code</span>',
											'<span class="link" id="csv">csv</span>',
										'</div>',
									'<div class="dataTables_wrapper form-inline dt-bootstrap">',
										'<!--script-->',
										table,
									'</div>',
									'<div class="footnote">' + '\n' + '%s'%(footnote) + '</div>',
										html_plots,
									'</div>',
								'</div>',
							'</div>',
						'</div>']
				#replace script within body, then join as string
				if ((source == "logit") or (source == "onset") or (source == "anova")):
					body = [script if x==(                '<!--script-->') else x for x in body]
				# join
				body = ('\n\t'.join(map(str, body)))

				#build footer
				if ((source == "logit") or (source == "onset") or (source == "anova")):
					foot = ['<script>',
								'$(document).ready(function(){',
									'getLogitTable();',
									'additionalTable();',
									'popover();',
									'codeButton();',
								'})',
							'</script>']
				elif (source == "task"):
					foot = ['<script>',
								'$(document).ready(function(){',
									'getLogitTable();',
									'popover();',
								'})',
							'</script>']   
				else:
					foot = ['<script>',
								'$(document).ready(function(){',
									'getLogitTable();',
								'})',
							'</script>']
				foot = ('\n\t'.join(map(str, foot)))
				
				#build html
				html = ['<html short="%s" long="%s">'%(short, long),
						'<head>',
							head,
						'</head>',
						'<body id="body">',
							body,
						'</body>',
						'<foot>',
							foot,
						'</foot>',
						'</html>']
				html = ('\n'.join(map(str, html)))
					
		#----save
		# check if paths exist
		if not os.path.exists(os.path.dirname(path)):
			os.makedirs(os.path.dirname(path))
		# tidy html
		#html = re.sub("\s\s+", " ", html) #multiple spaces
		# save
		with open(path,'w') as html_:
			html_.write(html)
		console('saved html file at %s'%(path), 'blue')
		
		#--------finished
		console('%s finished in %s msec'%(_f,((datetime.datetime.now()-_t0).total_seconds()*1000)), 'blue')
		return html
