def _get_linked_plots(backend: str = "plotly") -> Tuple: """Returns a tuple (scatter, hist) of linked plots Args: backend (str, optional): "plotly" or "bokeh". Defaults to "plotly". Returns: [Tuple]: Returns a tuple (scatter, hist) of linked plots """ dataset = hv.Dataset(IRIS_DATASET) scatter = hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"]) hist = hv.operation.histogram(dataset, dimension="petal_width", normed=False) # pylint: disable=no-value-for-parameter selection_linker = hv.selection.link_selections.instance() scatter = selection_linker(scatter).opts( opts.Scatter(**OPTS["all"]["scatter"], **OPTS[backend]["scatter"])) hist = selection_linker(hist).opts( opts.Histogram(**OPTS["all"]["hist"], **OPTS[backend]["hist"])) return scatter, hist
def dist_compare_grid( df1, df2, columns=None, max_bar_categories: int = 40, grid_size: Tuple[int, int] = (900, 900), ): if columns is None: columns = [c for c in df1.columns if c in df2.columns] else: for c in columns: if c not in df1.columns: raise ValueError("%s is not in df1.columns" % str(c)) if c not in df2.columns: raise ValueError("%s is not in df2.columns" % str(c)) grid_cols = int(np.ceil(np.sqrt(len(columns)))) grid_rows = int(np.ceil(len(columns) / grid_cols)) plots = [ dist_compare_plot(df1[c], df2[c], max_bar_categories).opts(title=c) for c in columns ] grid = hv.Layout(plots).opts(shared_axes=False, normalize=False) grid.cols(grid_cols) # set sizes subplot_size = (int(grid_size[0] / grid_cols), int(grid_size[1] / grid_rows)) grid.opts( opts.Histogram(width=subplot_size[0], height=subplot_size[1]), opts.Bars(width=subplot_size[0], height=subplot_size[1]), ) return grid
def generateCombined(self, decimated, SSC, FSC, cachebust, counter_max=20): renderer = hv.renderer('bokeh') body = None points = None point_collect = [] for key in decimated.keys(): print(key) point = hv.Scatter(decimated[key], SSC, FSC, label=key) point_collect.append(point) if points is None: points = point else: points *= point if body is None: body = points.opts(title='Default {0}: SSC vs FSC'.format("Combined"), height=450, width=450) else: body += points.opts(title='Default {0}: SSC vs FSC'.format("Combined")) for dim in (SSC, FSC): hists = None for point in point_collect: hist = histogram(point, dimension=dim) if hists is None: hists = hist else: hists *= hist body += hists potentialCols = [c for c in decimated[list(decimated.keys())[0]].columns if c != SSC and c != FSC] for i in range(len(potentialCols)): for j in range(i+1, len(potentialCols)): points = None point_collect = [] for key in decimated.keys(): point = hv.Scatter(decimated[key], potentialCols[i], potentialCols[j], label=key) point_collect.append(point) if points is None: points = point else: points *= point body += points.opts(title='Combined: {0} vs {1}'.format(potentialCols[i], potentialCols[j]), height=450, width=450) for dim in (potentialCols[i], potentialCols[j]): hists = None for point in point_collect: hist = histogram(point, dimension=dim) if hists is None: hists = hist else: hists *= hist body += hists body = body.opts( opts.Scatter(alpha=0.9), opts.Histogram(alpha=0.9, height=450), opts.Layout(shared_axes=True, shared_datasource=True)).cols(3) renderer.save(body, os.path.join(self.directory, cachebust+"combined_gating"))
def modify_doc(doc): points = hv.Points(np.random.randn(10000, 2)) points2 = hv.Points(np.random.randn(100, 2) * 2 + 1) xhist, yhist = (histogram(points2, bin_range=(-5, 5), dimension=dim) * histogram(points, bin_range=(-5, 5), dimension=dim) for dim in 'xy') composition = (points2 * points) << yhist.opts(width=125) << xhist.opts(height=125) composition.opts(opts.Histogram(alpha=0.3)) final = composition plot = renderer.get_plot(final) layout = row(plot.state) # renderer.server_doc(layout) doc.add_root(layout)
def Histogram(self): """ Bokeh alternative histogram """ # hist, edges = np.histogram(self.neosData[['Affinity']], density=True, bins=50) # p = figure(height=400) # p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], # fill_color="black", line_color="white", alpha=0.5) # p.xaxis.axis_label = "Binding Affinity (nM)" # p.yaxis.axis_label = "Frequency" data = self.neosData.loc[self.neosData['Affinity'] <= 500].copy() df = hv.Dataset(data) o = df.hist(dimension='Affinity', groupby='Sample', bins=50, adjoin=False) o.opts(opts.Histogram(alpha=0.9, height=200)) p = hv.render(o) return (p)
def config_layout(PlotItem, **kwargs): """Configs the layout of the output""" for key, value in kwargs.items(): try: getattr(PlotItem, key)(value) except AttributeError as err: log.warning( "Option '{}' for plot not possible with error: {}".format( key, err)) try: TOOLTIPS = [("File", "@Name"), ("index", "$index"), ("(x,y)", "($x, $y)")] hover = HoverTool(tooltips=TOOLTIPS) PlotItem.opts( opts.Curve(tools=[hover], toolbar="disable"), opts.Scatter(tools=[hover], toolbar="disable"), opts.Histogram(tools=[hover], toolbar="disable"), opts.Points(tools=[hover], toolbar="disable"), opts.BoxWhisker(tools=[hover], toolbar="disable"), opts.Bars(tools=[ HoverTool(tooltips=[('Value of ID:', ' $x'), ('Value:', '$y')]) ], toolbar="disable"), opts.Violin(tools=[hover], toolbar="disable")) except AttributeError as err: log.error( "Nonetype object encountered while configuring final plots layout. This should not happen! Error: {}" .format(err)) except ValueError as err: if "unexpected option 'tools'" in str(err).lower( ) or "unexpected option 'toolbar'" in str(err).lower(): pass else: raise return PlotItem
def create_doc(doc, data_dir): def get_spectrogram(s0=None, s1=None, size=1024, overlap=1. / 8, zfill=1, mode='psd'): s_size = np.dtype(np.complex64).itemsize if s1 is None and s0 is None: ds = None elif s1 is None: ds = None elif s0 is None: ds = np.abs(s1) else: ds = np.abs(s1 - s0) if ds is not None and s0 is not None: flen = getsize(join(data_dir, file)) print("file size: {} bytes".format(flen)) if (ds + s0) * s_size > flen: ds = flen - s0 * s_size samples = np.memmap(join(data_dir, file), dtype='complex64', mode='r', offset=s0 * s_size, shape=(ds, ) if ds else None) if ds is None: ds = len(samples) if ds / size > (res + 0.5) * height: noverlap = -int(float(size) * float(ds) / size / height / res) else: noverlap = size // (1. / overlap) f, t, S = signal.spectrogram(samples, samp_rate, nperseg=size, nfft=int( next_power_of_2(size) * int(zfill)), noverlap=noverlap, return_onesided=False, scaling='density', mode=mode) # f = fftshift(f) S = fftshift(S, axes=(0, )) if mode == 'psd': S = 10 * np.log10(S) return f, t, S, samples def get_spectrogram_img(z_min, z_max, tf_r, zfill, overlap, show_realfreq, freq_unit, x_range, y_range): lock.acquire() show_realfreq = bool(show_realfreq) hv_image = None try: print("y_range:", y_range, type(y_range)) if type(y_range[0]) != float: if np.issubdtype(y_range[0], np.datetime64) and time is not None: y_range = [ (y - np.datetime64(time) ).astype('timedelta64[us]').astype('float') / 1e6 for y in y_range ] #elif np.issubdtype(y0, np.timedelta64): # y0, y1 = [y.astype('timedelta64[s]').astype('float') for y in [y0,y1]] # tranform back to relative frequency if required print(doc.session_context.show_realfreq, x_range) last_freq_unit = doc.session_context.freq_unit x_range = [x * freq_units_names[last_freq_unit] for x in x_range] if doc.session_context.show_realfreq: x_range = [(x - freq) for x in x_range] print(doc.session_context.show_realfreq, "after transform", x_range) (x0, x1), (y0, y1) = x_range, y_range #print("y0 dtype:", y0.dtype) s0, s1 = sorted([ min(max(int(yr * samp_rate), 0), total_samples) for yr in [y0, y1] ]) scale = samp_rate / np.abs(x_range[1] - x_range[0]) size = int(width * scale) # required freq resolution to fulfill zoom level ds = np.abs( s1 - s0) # number of samples covered at the current zoom level if ds / size < height: size = int(np.sqrt(ds * scale * 10**tf_r)) f, t, S, samples = get_spectrogram( s0, s1, size=size, overlap=overlap, mode=mode, zfill=zfill if size < res * width else 1) t += max(min(y0, y1), 0) f_range = (x0 <= f) & (f <= x1) image = S[f_range, :] f = f[f_range] #if ds / size > height: # image = signal.resample(image, height*2, axis=0) print(image.shape) if intp_enabled: ratio = np.array(image.shape, dtype=np.float) / np.array( (height * res, width * res)) if np.min(ratio) < 1: scale = np.max( np.abs(image) ) # normalization factor for image because rescale needs that image = rescale(image / scale, 1. / np.min(ratio), order=1) * scale f = signal.resample(f, image.shape[0]) t = signal.resample(t, image.shape[1]) print("after resampling: ", image.shape) del samples #image = hv.Image(image, bounds=(x0, y0, x1, y1)).redim.range(z=(z_min, z_max)) # TODO get exact values in bounds if show_realtime and time is not None: t = np.datetime64(time) + np.array( t * 1e6).astype('timedelta64[us]') #else: # t = (t*1e6) #.astype('timedelta64[us]') if show_realfreq: f += freq f /= freq_units_names[freq_unit] #image = image.astype('float16') # trying to reduce network bandwidth... print("image dtype:", image.dtype) #hv_image = hv.Image(np.flip(image, axis=1), bounds=(min(f), max(t), max(f), min(t))) \ hv_image = hv.Image(xr.DataArray(image, coords=[f,t], dims=['f','t'], name='z'), ['f','t'], 'z') \ .options( xlabel="Frequency [{}]".format(freq_unit), ylabel="Time " + '[s]' if not show_realtime else '') \ .redim.range(z=(z_min, z_max)) if doc.session_context.show_realfreq != show_realfreq or doc.session_context.freq_unit != freq_unit: hv_image = hv_image \ .redim.range(f=(min(f), max(f))) \ .redim.range(t=(min(t), max(t))) print("redimming axis range") doc.session_context.show_realfreq = show_realfreq doc.session_context.freq_unit = freq_unit except Exception as e: print("Exception in image generation:", e) print(traceback.format_exc()) lock.release() return hv_image def get_param(name, default, t=str): try: args = doc.session_context.request.arguments if t == str: p = str(args.get(name)[0], 'utf-8') else: p = t(args.get(name)[0]) except: p = default return p time = None freq = 0 file = basename(get_param('file', '', str)) skip = get_param('skip', 0, int) keep = get_param('keep', None, int) # low resolution for raspberry width, height = get_param('width', 600, int), get_param('height', 500, int) res = get_param('res', 1.5, float) ds_enabled = get_param('ds', None, str) # datashade option (default: avg) intp_enabled = get_param('intp', 0, int) # interpolation enable flap show_realtime = bool(get_param('rt', 0, int)) # show real time on vertical axis mode = get_param('mode', 'psd', str) t_range = get_param('t', None, str) f_range = get_param('f', None, str) if t_range is not None: try: parts = t_range.split(",") if len(parts) == 2: t_range = list(map(float, parts)) except: pass if f_range is not None: try: parts = f_range.split(",") if len(parts) == 2: f_range = list(map(float, parts)) elif len(parts) == 1: _f = abs(float(parts[0])) f_range = (-_f, _f) except: pass if file.endswith(".meta"): config = ConfigParser() config.read(join(data_dir, file)) file = splitext(file)[0] + ".raw" samp_rate = int(config['main']['samp_rate']) freq = float(config['main']['freq']) time = datetime.fromtimestamp(float(config['main']['time'])) else: samp_rate = get_param('samp_rate', 128000, int) f, t, S, samples = get_spectrogram(s0=skip * samp_rate, s1=keep * samp_rate if keep else None, size=width, mode=mode) total_samples = len(samples) del samples # default is True if show_realtime and time is not None: t = np.datetime64(time) + np.array(t).astype('timedelta64[s]') doc.session_context.show_realfreq = False doc.session_context.freq_unit = freq_units[1000] range_stream = RangeXY(x_range=tuple( x / freq_units_names[doc.session_context.freq_unit] for x in ((min(f_range), max(f_range)) if f_range else (min(f), max(f)))), y_range=(max(t_range), min(t_range)) if t_range else (max(t), min(t))) # transient=True z_range = (np.min(S), np.max(S)) z_init = np.percentile(S, (50, 100)) dmap = hv.DynamicMap( get_spectrogram_img, streams=[range_stream], kdims=[ hv.Dimension('z_min', range=z_range, default=z_init[0]), hv.Dimension('z_max', range=z_range, default=z_init[1]), hv.Dimension('tf_r', label='Time-Frequency pixel ratio', range=(-10., 10.), default=0.), hv.Dimension('zfill', label='Zero-filling factor', range=(1, 10), default=2), hv.Dimension('overlap', label='Overlap factor', range=(-1., 1.), default=1. / 8), #hv.Dimension('show_realtime', label='Show real time on vertical axis', range=(0,1), default=0), hv.Dimension('show_realfreq', label='Show real frequency', range=(0, 1), default=int(doc.session_context.show_realfreq)), hv.Dimension('freq_unit', label='Frequency unit', values=list( map(lambda x: freq_units[x], sorted(freq_units.keys()))), default=doc.session_context.freq_unit), #hv.Dimension('mode', label='Spectrogram mode', values=['psd', 'angle', 'phase', 'magnitude'], default='psd') ]).options( framewise=True, # ??? ) #.redim.range(z=z_init) #dmap = dmap.opts(opts.Image(height=height, width=width)) if ds_enabled != None: print("datashade enabled: yes") if ds_enabled == "" or ds_enabled == "mean": ds_enabled = dsr.mean elif ds_enabled == "max": ds_enabled = dsr.max else: print( "warning: invalid option for datashade. using default value: mean" ) ds_enabled = dsr.mean dmap = regrid(dmap, aggregator=ds_enabled, interpolation='linear', upsample=True, height=height * 2, width=width * 2) # aggregation=dsr.max #dmap = dmap.hist(num_bins=150, normed=False) dmap = dmap.opts( opts.Image( cmap='viridis', framewise=True, colorbar=True, height=height, width=width, tools=['hover'], title='{}, {} {} sps'.format( time.strftime('%Y-%m-%d %H:%M:%S') if time else 'Time unknown', format_freq(freq), samp_rate)), opts.Histogram(framewise=False, width=150)) #plot = renderer.get_plot(hist, doc).state #widget = renderer.get_widget(hist, None, position='right').state #hvobj = layout([plot, widget]) #plot = layout([renderer.get_plot(hist, doc).state]) #doc.add_root(plot) doc = renderer.server_doc(dmap, doc=doc) doc.title = 'Waterfall Viewer'
def get_fractal(x_range, y_range): (x0, x1), (y0, y1) = x_range, y_range image = np.zeros((600, 600), dtype=np.uint8) return hv.Image(create_fractal(x0, x1, -y1, -y0, image, 200), bounds=(x0, y0, x1, y1)) # Define stream linked to axis XY-range range_stream = RangeXY(x_range=(-1., 1.), y_range=(-1., 1.)) # Create DynamicMap to compute fractal per zoom range and # adjoin a logarithmic histogram dmap = hv.DynamicMap(get_fractal, label='Manderbrot Explorer', streams=[range_stream]).hist(log=True) # Apply options dmap.opts( opts.Histogram(framewise=True, logy=True, width=200), opts.Image(cmap='fire', logz=True, height=600, width=600, xaxis=None, yaxis=None)) doc = renderer.server_doc(dmap) doc.title = 'Mandelbrot Explorer'
def outputCliqueDistribution(self, minCorr=None, countDuplicates=False, makeHistogram=False, fileName='cliqueHistogram', graphTitle='Class Correlation Cliques', logScale=False, exportPNG=True): """Outputs the clique distribution from the given correlation data. Prints to console by default, but can also optionally export a histogram. Args: minCorr (:obj:`None` or :obj:`float`, optional): Minimum correlation to consider a correlation an edge on the graph. 'None', or ignored, by default. countDuplicates (:obj:`bool`, optional): Whether or not to count smaller sub-cliques of larger cliques as cliques themselves. :obj:`False` by default. makeHistogram (:obj:`bool`, optional): Whether or not to generate a histogram. False by default. fileName (:obj:`str`, optional): File name to give exported histogram files. 'cliqueHistogram' by default. graphTitle (:obj:`str`, optional): Title displayed on the histogram. 'Class Correlation Cliques' by default. logScale (:obj:`bool`, optional): Whether or not to output graph in Log 10 scale on the y-axis. Defaults to :obj:`False`. """ # M: Gets a list of cliques, then the largest clique length cliques = self.getCliques(minCorr=minCorr) largestClique = len(max(cliques, key=len)) # M: makes a 'weight' array: contains counts of cliques of every size from 2 to 'largestClique' # (also includes count of smaller sub-cliques of larger cliques if 'countDuplicates' is true) weight = [] for k in range(2, largestClique + 1): count = 0 for clique in cliques: if len(clique) == k: count += 1 elif countDuplicates and len(clique) > k: count += len(list(itertools.combinations(clique, k))) weight.append(count) print('Size ' + str(k) + ' cliques: ' + str(count)) if makeHistogram: # cliqueCount = [len(clique) for clique in cliques] # frequencies, edges = np.histogram(cliqueCount, largestClique - 1, (2, largestClique)) cliqueCount = range(2, largestClique + 1) frequencies, edges = np.histogram(a=cliqueCount, bins=largestClique - 1, range=(2, largestClique), weights=weight) #print('Values: %s, Edges: %s' % (frequencies.shape[0], edges.shape[0])) ylbl = 'Number of Cliques' # M: sets a log scale if specified if logScale: frequencies = [math.log10(freq) for freq in frequencies] ylbl += ' (log 10 scale)' # M: creates histogram using (edges, frequencies) and customizes it histo = hv.Histogram((edges, frequencies)) histo.opts( opts.Histogram(xlabel='Number of Classes in Clique', ylabel=ylbl, title=graphTitle)) hv.output(size=125) subtitle = 'n = ' + str(self.getEntryCount()) if minCorr: subtitle = 'corr >= ' + str(minCorr) + ', ' + subtitle # M: creates the histogram, saves it, then shows it graph = hv.render(histo) graph.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="10pt"), 'above') output_file(outDir + fileName + '.html', mode='inline') save(graph) show(graph) # JH: Use exportPng=True to get rid of the global variable. # if not edmApplication: # M: png version if exportPNG: histo.opts(toolbar=None) graph = hv.render(histo) graph.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="10pt"), 'above') export_png(graph, filename=outDir + fileName + '.png')
def seriesToHistogram(data, fileName='histogram', graphTitle='Distribution', sortedAscending=True, logScale=False, xlbl='Value', ylbl='Frequency'): data2 = data.replace(' ', np.nan) data2.dropna(inplace=True) # data2.sort_values(inplace=True) try: histData = pd.to_numeric(data2, errors='raise') numericData = True except: histData = data2 numericData = False if numericData: # frequencies, edges = np.histogram(gpas, int((highest - lowest) / 0.1), (lowest, highest)) dataList = histData.tolist() frequencies, edges = np.histogram(dataList, (int(math.sqrt(len(dataList))) if (len(dataList) > 30) else (max(len(dataList) // 3, 1))), (min(dataList), max(dataList))) #print('Values: %s, Edges: %s' % (frequencies.shape[0], edges.shape[0])) if logScale: frequencies = [ math.log10(freq) if freq > 0 else freq for freq in frequencies ] ylbl += ' (log 10 scale)' histo = hv.Histogram((edges, frequencies)) histo.opts( opts.Histogram(xlabel=xlbl, ylabel=ylbl, title=graphTitle, fontsize={ 'title': 40, 'labels': 20, 'xticks': 20, 'yticks': 20 })) subtitle = 'mean: ' + str(round(sum(dataList) / len(dataList), 3)) + ', n = ' + str(len(dataList)) hv.output(size=250) graph = hv.render(histo) graph.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="30pt"), 'above') output_file(outDir + fileName + '.html', mode='inline') save(graph) show(graph) # JH: Adds some specific display components when not in a graphical program. # JH: Consider a separate function for the two cases. if not edmApplication: hv.output(size=300) histo.opts(toolbar=None) graph = hv.render(histo) graph.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="30pt"), 'above') export_png(graph, filename=outDir + fileName + '.png') else: barData = histData.value_counts(dropna=False) dictList = sorted(zip(barData.index, barData.values), key=lambda x: x[sortedAscending]) # print(dictList) bar = hv.Bars(dictList) bar.opts(opts.Bars(xlabel=xlbl, ylabel=ylbl, title=graphTitle)) subtitle = 'n = ' + str(len(dictList)) hv.output(size=250) graph = hv.render(bar) graph.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="30pt"), 'above') output_file(outDir + fileName + '.html', mode='inline') save(graph) show(graph) # JH: Consider a bool exportPng=True when calling from outside edmAppliation if not edmApplication: hv.output(size=300) bar.opts(toolbar=None) graph2 = hv.render(bar) graph2.add_layout( Title(text=subtitle, text_font_style="italic", text_font_size="30pt"), 'above') export_png(graph2, filename=outDir + fileName + '.png') hv.output(size=125)
df = iris() dataset = hv.Dataset(df) # Build selection linking object selection_linker = hv.selection.link_selections.instance() scatter = selection_linker( hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"])) hist = selection_linker( hv.operation.histogram(dataset, dimension="petal_width", normed=False)) # Use plot hook to set the default drag mode to box selection def set_dragmode(plot, element): fig = plot.state fig['layout']['dragmode'] = "select" if isinstance(element, hv.Histogram): # Constrain histogram selection direction to horizontal fig['layout']['selectdirection'] = "h" scatter.opts(opts.Scatter(hooks=[set_dragmode])) hist.opts(opts.Histogram(hooks=[set_dragmode])) app = dash.Dash(__name__) components = to_dash(app, [scatter, hist], reset_button=True) app.layout = html.Div(components.children) if __name__ == "__main__": app.run_server(debug=True)
clr_5cvMean = 'maroon' clr_20x5cvMean = 'darkgoldenrod' default_fontsizes = dict(title=8, labels=8, ticks=7, minor_ticks=7, legend=7) fig_opts = [ opts.Layout(aspect_weight=1, fig_inches=(3.42, None), sublabel_size=10, fontsize=8), opts.Overlay(fontsize=default_fontsizes, ), opts.Area(fontsize=default_fontsizes), opts.Arrow(textsize=default_fontsizes), opts.Curve(fontsize=default_fontsizes), opts.HexTiles(fontsize=default_fontsizes), opts.Histogram(fontsize=default_fontsizes), opts.Raster(fontsize=default_fontsizes), opts.Scatter(fontsize=default_fontsizes), opts.Text(fontsize=default_fontsizes), opts.QuadMesh(fontsize=default_fontsizes), opts.Violin(fontsize=default_fontsizes), opts.VLine(fontsize=default_fontsizes), ] # hv hooks class Suptitle: def __init__(self, suptitle, color, y=1.175): self.suptitle = suptitle self.color = color
def get_fractal(x_range, y_range): (x0, x1), (y0, y1) = x_range, y_range image = np.zeros((600, 600), dtype=np.uint8) return hv.Image(create_fractal(x0, x1, -y1, -y0, image, 200), bounds=(x0, y0, x1, y1)) # Define stream linked to axis XY-range range_stream = RangeXY(x_range=(-1., 1.), y_range=(-1., 1.)) # Create DynamicMap to compute fractal per zoom range and # adjoin a logarithmic histogram dmap = hv.DynamicMap(get_fractal, label='Manderbrot Explorer', streams=[range_stream]).hist(log=True) # Apply options dmap.opts( opts.Histogram(framewise=True, logy=True, width=200, xlim=(1, None)), opts.Image(cmap='fire', logz=True, height=600, width=600, xaxis=None, yaxis=None)) doc = renderer.server_doc(dmap) doc.title = 'Mandelbrot Explorer'