Source code for masci_tools.vis.bokeh_plots

###############################################################################
# Copyright (c), Forschungszentrum Jülich GmbH, IAS-1/PGI-1, Germany.         #
#                All rights reserved.                                         #
# This file is part of the Masci-tools package.                               #
# (Material science tools)                                                    #
#                                                                             #
# The code is hosted on GitHub at https://github.com/judftteam/masci-tools.   #
# For further information on the license, see the LICENSE.txt file.           #
# For further information please visit http://judft.de/.                      #
#                                                                             #
###############################################################################
"""
Here are general and special bokeh plots to use

"""
from .bokeh_plotter import BokehPlotter
from .parameters import ensure_plotter_consistency, NestedPlotParameters
from .data import process_data_arguments

from .helpers import get_special_kpoint_ticks

import pandas as pd
import numpy as np
import warnings
from pprint import pprint

################## Helpers     ################

plot_params = BokehPlotter()


[docs]def set_bokeh_plot_defaults(**kwargs): """ Set defaults for bokeh backend according to the given keyword arguments Available defaults can be seen in :py:class:`~masci_tools.vis.bokeh_plotter.BokehPlotter` """ plot_params.set_defaults(**kwargs)
[docs]def reset_bokeh_plot_defaults(): """ Reset the defaults for bokeh backend to the hardcoded defaults Available defaults can be seen in :py:class:`~masci_tools.vis.bokeh_plotter.BokehPlotter` """ plot_params.reset_defaults()
[docs]def show_bokeh_plot_defaults(): """ Show the currently set defaults for bokeh backend Available defaults can be seen in :py:class:`~masci_tools.vis.bokeh_plotter.BokehPlotter` """ pprint(plot_params.get_dict())
[docs]def get_bokeh_help(key): """ Print the description of the given key in the bokeh backend Available defaults can be seen in :py:class:`~masci_tools.vis.bokeh_plotter.BokehPlotter` """ plot_params.get_description(key)
[docs]def load_bokeh_defaults(filename='plot_bokeh_defaults.json'): """ Load defaults for the bokeh backend from a json file. :param filename: filename,from where the defaults should be taken """ plot_params.load_defaults(filename)
[docs]def save_bokeh_defaults(filename='plot_bokeh_defaults.json', save_complete=False): """ Save the current defaults for the matplotlib backend to a json file. :param filename: filename, where the defaults should be stored :param save_complete: bool if True not only the overwritten user defaults but also the unmodified hardcoded defaults are stored """ plot_params.save_defaults(filename, save_complete=save_complete)
##################################### general plots ##########################
[docs]@ensure_plotter_consistency(plot_params) def bokeh_scatter(x, y=None, *, xlabel='x', ylabel='y', title='', figure=None, data=None, saveas='scatter', copy_data=False, **kwargs): """ Create an interactive scatter plot with bokeh :param x: arraylike or key for data for the x-axis :param y: arraylike or key for data for the y-axis :param data: source for the data of the plot (pandas Dataframe for example) :param xlabel: label for the x-axis :param ylabel: label for the y-axis :param title: title of the figure :param figure: bokeh figure (optional), if provided the plot will be added to this figure :param outfilename: filename of the output file :param copy_data: bool, if True the data argument will be copied Kwargs will be passed on to :py:class:`masci_tools.vis.bokeh_plotter.BokehPlotter`. If the arguments are not recognized they are passed on to the bokeh function `scatter` """ from bokeh.models import ColumnDataSource if isinstance(x, (dict, pd.DataFrame, ColumnDataSource)) or x is None: warnings.warn( 'Passing the source as first argument is deprecated. Please pass in source by the keyword data' 'and xdata and ydata as the first arguments', DeprecationWarning) data = x x = kwargs.pop('xdata', 'x') y = kwargs.pop('ydata', 'y') plot_data = process_data_arguments(data=data, x=x, y=y, copy_data=copy_data, single_plot=True, same_length=True, use_column_source=True) entry, source = plot_data.items(first=True) plot_params.set_defaults(default_type='function', name=entry.y) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, xlabel, ylabel, figure=figure) plot_kwargs = plot_params.plot_kwargs(plot_type='scatter') res = p.scatter(x=entry.x, y=entry.y, source=source, **plot_kwargs, **kwargs) plot_params.add_tooltips(p, res, entry) if plot_params['level'] is not None: res.level = plot_params['level'] plot_params.draw_straight_lines(p) plot_params.set_limits(p) plot_params.save_plot(p, saveas) return p
[docs]@ensure_plotter_consistency(plot_params) def bokeh_multi_scatter(x, y=None, *, data=None, figure=None, xlabel='x', ylabel='y', title='', saveas='scatter', copy_data=False, set_default_legend=True, **kwargs): """ Create an interactive scatter (muliple data sets possible) plot with bokeh :param x: arraylike or key for data for the x-axis :param y: arraylike or key for data for the y-axis :param data: source for the data of the plot (pandas Dataframe for example) :param xlabel: label for the x-axis :param ylabel: label for the y-axis :param title: title of the figure :param figure: bokeh figure (optional), if provided the plot will be added to this figure :param outfilename: filename of the output file :param copy_data: bool, if True the data argument will be copied :param set_default_legend: bool if True the data names are used to generate default legend labels Kwargs will be passed on to :py:class:`masci_tools.vis.bokeh_plotter.BokehPlotter`. If the arguments are not recognized they are passed on to the bokeh function `scatter` """ from bokeh.models import ColumnDataSource if isinstance(x, (dict, pd.DataFrame, ColumnDataSource)) or x is None: warnings.warn( 'Passing the source as first argument is deprecated. Please pass in source by the keyword data' 'and xdata and ydata as the first arguments', DeprecationWarning) data = x x = kwargs.pop('xdata', 'x') y = kwargs.pop('ydata', 'y') plot_data = process_data_arguments(data=data, x=x, y=y, same_length=True, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if plot_data.distinct_datasets('x') == 1: default_legend_label = plot_data.get_keys('y') else: default_legend_label = plot_data.get_keys('x') if set_default_legend: plot_params.set_defaults(default_type='function', legend_label=default_legend_label) plot_params.set_defaults(default_type='function', name=default_legend_label) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, xlabel, ylabel, figure=figure) #Process the given color arguments plot_params.set_color_palette_by_num_plots() plot_kwargs = plot_params.plot_kwargs(plot_type='scatter') for indx, ((entry, source), plot_kw) in enumerate(zip(plot_data.items(), plot_kwargs)): res = p.scatter(x=entry.x, y=entry.y, source=source, **plot_kw, **kwargs) plot_params.add_tooltips(p, res, entry) if plot_params[('level', indx)] is not None: res.level = plot_params[('level', indx)] plot_params.draw_straight_lines(p) plot_params.set_limits(p) plot_params.set_legend(p) plot_params.save_plot(p, saveas) return p
[docs]@ensure_plotter_consistency(plot_params) def bokeh_line(x, y=None, *, data=None, figure=None, xlabel='x', ylabel='y', title='', saveas='line', plot_points=False, area_curve=0, copy_data=False, set_default_legend=True, **kwargs): """ Create an interactive multi-line plot with bokeh :param x: arraylike or key for data for the x-axis :param y: arraylike or key for data for the y-axis :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param xlabel: label for the x-axis :param ylabel: label for the y-axis :param title: title of the figure :param figure: bokeh figure (optional), if provided the plot will be added to this figure :param outfilename: filename of the output file :param plot_points: bool, if True also plot the points with a scatterplot on top :param copy_data: bool, if True the data argument will be copied :param set_default_legend: bool if True the data names are used to generate default legend labels Kwargs will be passed on to :py:class:`masci_tools.vis.bokeh_plotter.BokehPlotter`. If the arguments are not recognized they are passed on to the bokeh function `line` """ from bokeh.models import ColumnDataSource if isinstance(x, (dict, pd.DataFrame, ColumnDataSource)) or x is None: warnings.warn( 'Passing the source as first argument is deprecated. Please pass in source by the keyword data' 'and xdata and ydata as the first arguments', DeprecationWarning) data = x x = kwargs.pop('xdata', 'x') y = kwargs.pop('ydata', 'y') plot_data = process_data_arguments(data=data, x=x, y=y, shift=area_curve, same_length=True, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if plot_data.distinct_datasets('x') == 1: default_legend_label = plot_data.get_keys('y') else: default_legend_label = plot_data.get_keys('x') if set_default_legend: plot_params.set_defaults(default_type='function', legend_label=default_legend_label) plot_params.set_defaults(default_type='function', name=default_legend_label) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, xlabel, ylabel, figure=figure) #Process the given color arguments plot_params.set_color_palette_by_num_plots() plot_kw_line = plot_params.plot_kwargs(plot_type='line') plot_kw_scatter = plot_params.plot_kwargs(plot_type='scatter') plot_kw_area = plot_params.plot_kwargs(plot_type='area') area_curve = kwargs.pop('area_curve', None) for indx, ((entry, source), kw_line, kw_scatter, kw_area) in enumerate(zip(plot_data.items(), plot_kw_line, plot_kw_scatter, plot_kw_area)): if plot_params[('area_plot', indx)]: if plot_params[('area_vertical', indx)]: p.harea(y=entry.y, x1=entry.x, x2=entry.shift, **kw_area, source=source) else: p.varea(x=entry.x, y1=entry.y, y2=entry.shift, **kw_area, source=source) res = p.line(x=entry.x, y=entry.y, source=source, **kw_line, **kwargs) plot_params.add_tooltips(p, res, entry) res2 = None if plot_points: res2 = p.scatter(x=entry.x, y=entry.y, source=source, **kw_scatter) if plot_params[('level', indx)] is not None: res.level = plot_params[('level', indx)] if res2 is not None: res2.level = plot_params[('level', indx)] plot_params.draw_straight_lines(p) plot_params.set_limits(p) plot_params.set_legend(p) plot_params.save_plot(p, saveas) return p
[docs]@ensure_plotter_consistency(plot_params) def bokeh_dos(energy_grid, dos_data=None, *, data=None, energy_label='$$E-E_F [eV]$$', dos_label=r'DOS [1/eV]', title=r'Density of states', xyswitch=False, e_fermi=0, saveas='dos_plot', copy_data=False, **kwargs): """ Create an interactive dos plot (non-spinpolarized) with bokeh Both horizontal or vertical orientation are possible :param energy_grid: arraylike or key data for the energy grid :param spin_up_data: arraylike or key data for the DOS :param data: source for the DOS data (optional) of the plot (pandas Dataframe for example) :param energy_label: label for the energy-axis :param dos_label: label for the dos-axis :param title: title of the figure :param xyswitch: bool if True, the energy will be plotted along the y-direction :param e_fermi: float, determines, where to put the line for the fermi energy :param outfilename: filename of the output file :param copy_data: bool, if True the data argument will be copied Kwargs will be passed on to :py:func:`bokeh_line()` """ from bokeh.models import ColumnDataSource if isinstance(energy_grid, (dict, pd.DataFrame, ColumnDataSource)) or energy_grid is None: warnings.warn( 'Passing the dataframe as first argument is deprecated. Please pass in source by the keyword data' 'and energy_grid and dos_data as the first arguments', DeprecationWarning) data = energy_grid energy_grid = kwargs.pop('energy', 'energy_grid') dos_data = kwargs.pop('ynames', None) if dos_data is None and data is not None: dos_data = set(data.keys()) - set([energy_grid] if isinstance(energy_grid, str) else energy_grid) dos_data = sorted(dos_data) plot_data = process_data_arguments(data=data, energy=energy_grid, dos=dos_data, same_length=True, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if 'limits' in kwargs: limits = kwargs.pop('limits') if 'x' not in limits and 'y' not in limits: if xyswitch: limits['x'], limits['y'] = limits.pop('dos', None), limits.pop('energy', None) else: limits['x'], limits['y'] = limits.pop('energy', None), limits.pop('dos', None) kwargs['limits'] = {k: v for k, v in limits.items() if v is not None} lines = {'horizontal': 0} lines['vertical'] = e_fermi if xyswitch: lines['vertical'], lines['horizontal'] = lines['horizontal'], lines['vertical'] plot_params.set_defaults(default_type='function', straight_lines=lines, tooltips=[('Name', '$name'), ('Energy', '@{x}{{0.0[00]}}'), ('DOS value', '@$name{{0.00}}')], figure_kwargs={ 'width': 1000, }) if xyswitch: x, y = plot_data.get_keys('dos'), plot_data.get_keys('energy') xlabel, ylabel = dos_label, energy_label plot_params.set_defaults(default_type='function', area_vertical=True) else: xlabel, ylabel = energy_label, dos_label x, y = plot_data.get_keys('energy'), plot_data.get_keys('dos') p = bokeh_line(x, y, data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, name=y, saveas=saveas, **kwargs) return p
[docs]@ensure_plotter_consistency(plot_params) def bokeh_spinpol_dos(energy_grid, spin_up_data=None, spin_dn_data=None, *, data=None, spin_dn_negative=True, energy_label='$$E-E_F [eV]$$', dos_label=r'DOS [1/eV]', title=r'Density of states', xyswitch=False, e_fermi=0, spin_arrows=True, saveas='dos_plot', copy_data=False, **kwargs): """ Create an interactive dos plot (spinpolarized) with bokeh Both horizontal or vertical orientation are possible :param energy_grid: arraylike or key data for the energy grid :param spin_up_data: arraylike or key data for the DOS spin-up :param spin_dn_data: arraylike or key data for the DOS spin-dn :param data: source for the DOS data (optional) of the plot (pandas Dataframe for example) :param spin_dn_negative: bool, if True (default), the spin down components are plotted downwards :param energy_label: label for the energy-axis :param dos_label: label for the dos-axis :param title: title of the figure :param xyswitch: bool if True, the energy will be plotted along the y-direction :param e_fermi: float, determines, where to put the line for the fermi energy :param spin_arrows: bool, if True (default) small arrows will be plotted on the left side of the plot indicating the spin directions (if spin_dn_negative is True) :param outfilename: filename of the output file :param copy_data: bool, if True the data argument will be copied Kwargs will be passed on to :py:func:`bokeh_line()` """ from bokeh.models import NumeralTickFormatter, Arrow, NormalHead from bokeh.models import ColumnDataSource if isinstance(energy_grid, (dict, pd.DataFrame, ColumnDataSource)) or energy_grid is None: warnings.warn( 'Passing the dataframe as first argument is deprecated. Please pass in source by the keyword data' 'and energy_grid and dos_data as the first arguments', DeprecationWarning) data = energy_grid energy_grid = kwargs.pop('energy', 'energy_grid') spin_up_data = kwargs.pop('ynames', None) spin_up_data, spin_dn_data = spin_up_data[:len(spin_up_data) // 2], spin_up_data[len(spin_up_data) // 2:] if spin_up_data is None and data is not None: spin_up_data = {key for key in data.keys() if '_up' in key} spin_up_data = sorted(spin_up_data) spin_dn_data = {key for key in data.keys() if '_dn' in key} spin_dn_data = sorted(spin_dn_data) plot_data = process_data_arguments(data=data, energy=energy_grid, spin_up=spin_up_data, spin_dn=spin_dn_data, same_length=True, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if 'limits' in kwargs: limits = kwargs.pop('limits') if 'x' not in limits and 'y' not in limits: if xyswitch: limits['x'], limits['y'] = limits.pop('dos', None), limits.pop('energy', None) else: limits['x'], limits['y'] = limits.pop('energy', None), limits.pop('dos', None) kwargs['limits'] = {k: v for k, v in limits.items() if v is not None} lines = {'horizontal': 0} lines['vertical'] = e_fermi if spin_dn_negative: plot_data.apply('spin_dn', lambda x: -x) if xyswitch: lines['vertical'], lines['horizontal'] = lines['horizontal'], lines['vertical'] plot_params.set_defaults(default_type='function', straight_lines=lines, tooltips=[('DOS Name', '$name'), ('Energy', '@{x}{{0.0[00]}}'), ('Value', '@$name{{(0,0.00)}}')], figure_kwargs={'width': 1000}) #Create the full data for the scatterplot energy_entries = plot_data.get_keys('energy') * 2 dos_entries = plot_data.get_keys('spin_up') + plot_data.get_keys('spin_dn') sources = plot_data.data if isinstance(sources, list): sources = sources * 2 if xyswitch: x, y = dos_entries, energy_entries xlabel, ylabel = dos_label, energy_label plot_params.set_defaults(default_type='function', area_vertical=True, x_axis_formatter=NumeralTickFormatter(format='(0,0)')) else: xlabel, ylabel = energy_label, dos_label x, y = energy_entries, dos_entries plot_params.set_defaults(default_type='function', area_vertical=True, y_axis_formatter=NumeralTickFormatter(format='(0,0)')) plot_params.set_parameters(color=kwargs.pop('color', None), color_palette=kwargs.pop('color_palette', None)) plot_params.set_color_palette_by_num_plots() #Double the colors for spin up and down kwargs['color'] = list(plot_params['color']).copy() kwargs['color'].extend(kwargs['color']) if 'legend_label' not in kwargs: kwargs['legend_label'] = dos_entries else: if isinstance(kwargs['legend_label'], list): if len(kwargs['legend_label']) == len(plot_data): kwargs['legend_label'].extend(kwargs['legend_label']) if 'show' in kwargs: plot_params.set_parameters(show=kwargs.pop('show')) if 'save_plots' in kwargs: plot_params.set_parameters(save_plots=kwargs.pop('save_plots')) with NestedPlotParameters(plot_params): p = bokeh_line(x, y, xlabel=xlabel, ylabel=ylabel, title=title, data=sources, name=dos_entries, show=False, save_plots=False, **kwargs) if spin_arrows and spin_dn_negative: #These are hardcoded because the parameters are not #reused anywhere (for now) x_pos = 50 length = 70 pad = 30 height = p.height - 100 alpha = 0.5 p.add_layout( Arrow(x_start=x_pos, x_end=x_pos, y_start=height - pad - length, y_end=height - pad, start_units='screen', end_units='screen', line_width=2, line_alpha=alpha, end=NormalHead(line_width=2, size=10, fill_alpha=alpha, line_alpha=alpha))) p.add_layout( Arrow(x_start=x_pos, x_end=x_pos, y_start=pad + length, y_end=pad, start_units='screen', end_units='screen', line_width=2, line_alpha=alpha, end=NormalHead(line_width=2, size=10, fill_alpha=alpha, line_alpha=alpha))) plot_params.save_plot(p, saveas) return p
[docs]@ensure_plotter_consistency(plot_params) def bokeh_bands(kpath, bands=None, *, data=None, size_data=None, color_data=None, xlabel='', ylabel='$$E-E_F [eV]$$', title='', special_kpoints=None, markersize_min=3.0, markersize_scaling=10.0, saveas='bands_plot', scale_color=True, separate_bands=False, line_plot=False, band_index=None, copy_data=False, **kwargs): """ Create an interactive bandstructure plot (non-spinpolarized) with bokeh Can make a simple plot or weight the size and color of the points against a given weight :param kpath: arraylike or key data for the kpoint data :param bands: arraylike or key data for the eigenvalues :param size_data: arraylike or key data the weights to emphasize (optional) :param color_data: str or arraylike, data for the color values with a colormap (optional) :param data: source for the bands data (optional) of the plot (pandas Dataframe for example) :param xlabel: label for the x-axis (default no label) :param ylabel: label for the y-axis :param title: title of the figure :param special_kpoints: list of tuples (str, float), place vertical lines at the given values and mark them on the x-axis with the given label :param e_fermi: float, determines, where to put the line for the fermi energy :param markersize_min: minimum value used in scaling points for weight :param markersize_scaling: factor used in scaling points for weight :param outfilename: filename of the output file :param scale_color: bool, if True (default) the weight will be additionally shown via a colormapping :param line_plot: bool, if True the bandstructure will be plotted with lines Here no weights are supported :param separate_bands: bool, if True the bandstructure will be separately plotted for each band allows more specific parametrization :param band_index: data for which eigenvalue belongs to which band (needed for line_plot and separate_bands) :param copy_data: bool, if True the data argument will be copied Kwargs will be passed on to :py:func:`bokeh_multi_scatter()` or :py:func:`bokeh_line()` """ from bokeh.transform import linear_cmap from bokeh.models import ColumnDataSource if 'size_scaling' in kwargs: warnings.warn('size_scaling is deprecated. Use markersize_scaling instead', DeprecationWarning) markersize_scaling = kwargs.pop('size_scaling') if 'size_min' in kwargs: warnings.warn('size_min is deprecated. Use markersize_min instead', DeprecationWarning) markersize_min = kwargs.pop('size_min') if isinstance(kpath, (dict, pd.DataFrame, ColumnDataSource)) or kpath is None: warnings.warn( 'Passing the dataframe as first argument is deprecated. Please pass in source by the keyword data' 'and kpath and bands as the first arguments', DeprecationWarning) data = kpath kpath = kwargs.pop('k_label', 'kpath') bands = kwargs.pop('eigenvalues', 'eigenvalues_up') if 'weight' in kwargs: warnings.warn('The weight argument is deprecated. Use size_data and color_data instead', DeprecationWarning) size_data = kwargs.pop('weight') plot_data = process_data_arguments(single_plot=True, data=data, kpath=kpath, bands=bands, size=size_data, color=color_data, band_index=band_index, copy_data=copy_data, use_column_source=True) if line_plot and size_data is not None: raise ValueError('Bandstructure with lines and size scaling not supported') if line_plot and color_data is not None: raise ValueError('Bandstructure with lines and color mapping not supported') if line_plot or separate_bands: if band_index is None: raise ValueError('The data for band indices are needed for separate_bands and line_plot') plot_data.group_data('band_index') plot_data.sort_data('kpath') if scale_color and size_data is not None: if color_data is not None: raise ValueError('color_data should not be provided when scale_color is True') plot_data.copy_data('size', 'color', rename_original=True) if color_data is not None: kwargs['color'] = plot_data.get_keys('color') entries = plot_data.keys(first=True) if entries.size is not None: ylimits = (-15, 15) if 'limits' in kwargs: if 'y' in kwargs['limits']: ylimits = kwargs['limits']['y'] data = plot_data.values(first=True) mask = np.logical_and(data.bands > ylimits[0], data.bands < ylimits[1]) weight_max = plot_data.max('size', mask=mask) plot_params.set_defaults(default_type='function', marker_size=entries.size) if scale_color: plot_params.set_defaults(default_type='function', color=linear_cmap(entries.color, 'Blues256', weight_max, -0.05)) plot_data.apply('size', lambda size: markersize_min + markersize_scaling * size / weight_max) else: plot_params.set_defaults(default_type='function', color='black') xticks, xticklabels = get_special_kpoint_ticks(special_kpoints, math_mode='$$') pos_to_label = {} for pos, label in zip(xticks, xticklabels): if pos.is_integer(): pos_to_label[int(pos)] = label pos_to_label[pos] = label lines = {'horizontal': 0, 'vertical': xticks} limits = {'y': (-15, 15)} plot_params.set_defaults(default_type='function', straight_lines=lines, x_ticks=xticks, x_ticklabels_overwrite=pos_to_label, figure_kwargs={ 'width': 1280, 'height': 720 }, x_range_padding=0.0, y_range_padding=0.0, legend_label='Eigenvalues', limits=limits) if line_plot: return bokeh_line(plot_data.get_keys('kpath'), plot_data.get_keys('bands'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, set_default_legend=False, saveas=saveas, **kwargs) return bokeh_multi_scatter(plot_data.get_keys('kpath'), plot_data.get_keys('bands'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, set_default_legend=False, saveas=saveas, **kwargs)
[docs]@ensure_plotter_consistency(plot_params) def bokeh_spinpol_bands(kpath, bands_up=None, bands_dn=None, *, size_data=None, color_data=None, data=None, xlabel='', ylabel='$$E-E_F [eV]$$', title='', special_kpoints=None, markersize_min=3.0, markersize_scaling=10.0, saveas='bands_plot', scale_color=True, line_plot=False, separate_bands=False, band_index=None, copy_data=False, **kwargs): """ Create an interactive bandstructure plot (spinpolarized) with bokeh Can make a simple plot or weight the size and color of the points against a given weight :param kpath: arraylike or key data for the kpoint data :param bands_up: arraylike or key data for the eigenvalues spin-up :param bands_dn: arraylike or key data for the eigenvalues spin-dn :param size_data: arraylike or key data the weights to emphasize (optional) :param color_data: str or arraylike, data for the color values with a colormap (optional) :param data: source for the bands data (optional) of the plot (pandas Dataframe for example) :param xlabel: label for the x-axis (default no label) :param ylabel: label for the y-axis :param title: title of the figure :param special_kpoints: list of tuples (str, float), place vertical lines at the given values and mark them on the x-axis with the given label :param e_fermi: float, determines, where to put the line for the fermi energy :param markersize_min: minimum value used in scaling points for weight :param markersize_scaling: factor used in scaling points for weight :param outfilename: filename of the output file :param scale_color: bool, if True (default) the weight will be additionally shown via a colormapping :param line_plot: bool, if True the bandstructure will be plotted with lines Here no weights are supported :param separate_bands: bool, if True the bandstructure will be separately plotted for each band allows more specific parametrization :param band_index: data for which eigenvalue belongs to which band (needed for line_plot and separate_bands) :param copy_data: bool, if True the data argument will be copied Kwargs will be passed on to :py:func:`bokeh_multi_scatter()` or :py:func:`bokeh_line()` """ from bokeh.transform import linear_cmap from bokeh.models import ColumnDataSource if 'size_scaling' in kwargs: warnings.warn('size_scaling is deprecated. Use markersize_scaling instead', DeprecationWarning) markersize_scaling = kwargs.pop('size_scaling') if 'size_min' in kwargs: warnings.warn('size_min is deprecated. Use markersize_min instead', DeprecationWarning) markersize_min = kwargs.pop('size_min') if isinstance(kpath, (dict, pd.DataFrame, ColumnDataSource)) or kpath is None: warnings.warn( 'Passing the dataframe as first argument is deprecated. Please pass in source by the keyword data' 'and kpath and bands_up and bands_dn as the first arguments', DeprecationWarning) data = kpath kpath = kwargs.pop('k_label', 'kpath') bands_up = kwargs.pop('eigenvalues', ['eigenvalues_up', 'eigenvalues_down']) bands_up, bands_dn = bands_up[0], bands_up[1] if 'weight' in kwargs: warnings.warn('The weight argument is deprecated. Use size_data and color_data instead', DeprecationWarning) size_data = kwargs.pop('weight') plot_data = process_data_arguments(data=data, kpath=kpath, bands=[bands_up, bands_dn], size=size_data, color=color_data, band_index=band_index, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if len(plot_data) != 2: raise ValueError('Wrong number of plots specified (Only 2 permitted)') if line_plot and size_data is not None: raise ValueError('Bandstructure with lines and size scaling not supported') if line_plot and color_data is not None: raise ValueError('Bandstructure with lines and color mapping not supported') if line_plot or separate_bands: if band_index is None: raise ValueError('The data for band indices are needed for separate_bands and line_plot') plot_data.group_data('band_index') plot_data.sort_data('kpath') if scale_color and size_data is not None: if color_data is not None: raise ValueError('color_data should not be provided when scale_color is True') plot_data.copy_data('size', 'color', rename_original=True) if color_data is not None: kwargs['color'] = plot_data.get_keys('color') if any(entry.size is not None for entry in plot_data.keys()): ylimits = (-15, 15) if 'limits' in kwargs: if 'y' in kwargs['limits']: ylimits = kwargs['limits']['y'] data = plot_data.values() mask = [np.logical_and(col.bands > ylimits[0], col.bands < ylimits[1]) for col in data] weight_max = plot_data.max('size', mask=mask) plot_data.apply('size', lambda size: markersize_min + markersize_scaling * size / weight_max) plot_params.set_defaults(default_type='function', marker_size=plot_data.get_keys('size')) if scale_color: plot_params.set_defaults(default_type='function', color=[ linear_cmap(name, palette, weight_max, -0.05) for name, palette in zip(plot_data.get_keys('color'), ['Blues256', 'Reds256']) ]) else: color = ['blue', 'red'] plot_params.set_defaults(default_type='function', color=color) xticks, xticklabels = get_special_kpoint_ticks(special_kpoints, math_mode='$$') pos_to_label = {} for pos, label in zip(xticks, xticklabels): if pos.is_integer(): pos_to_label[int(pos)] = label pos_to_label[pos] = label lines = {'horizontal': 0, 'vertical': xticks} limits = {'y': (-15, 15)} plot_params.set_defaults(default_type='function', straight_lines=lines, x_ticks=xticks, x_ticklabels_overwrite=pos_to_label, figure_kwargs={ 'width': 1280, 'height': 720 }, x_range_padding=0.0, y_range_padding=0.0, limits=limits, legend_label=['Spin Up', 'Spin Down'], level=[None, 'underlay']) if line_plot or separate_bands: plot_params.num_plots = len(plot_data) kwargs = plot_params.expand_parameters(original_length=2, **kwargs) if line_plot: return bokeh_line(plot_data.get_keys('kpath'), plot_data.get_keys('bands'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, set_default_legend=False, saveas=saveas, **kwargs) return bokeh_multi_scatter(plot_data.get_keys('kpath'), plot_data.get_keys('bands'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, set_default_legend=False, saveas=saveas, **kwargs)
[docs]@ensure_plotter_consistency(plot_params) def bokeh_spectral_function(kpath, energy_grid, spectral_function, *, data=None, special_kpoints=None, e_fermi=0, xlabel='', ylabel='$$E-E_F [eV]$$', title='', saveas='spectral_function', copy_data=False, figure=None, **kwargs): """ Create a colormesh plot of a spectral function :param kpath: data for the kpoint coordinates :param energy_grid: data for the energy grid :param spectral_function: 2D data for the spectral function :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param title: str, Title of the plot :param xlabel: str, label for the x-axis :param ylabel: str, label for the y-axis :param saveas: str, filename for the saved plot :param e_fermi: float (default 0), place the line for the fermi energy at this value :param special_kpoints: list of tuples (str, float), place vertical lines at the given values and mark them on the x-axis with the given label :param copy_data: bool, if True the data argument will be copied All other Kwargs are passed on to the image call of bokeh """ plot_data = process_data_arguments(single_plot=True, data=data, kpath=kpath, energy=energy_grid, spectral_function=spectral_function, forbid_split_up={ 'spectral_function', }, copy_data=copy_data) xticks, xticklabels = get_special_kpoint_ticks(special_kpoints, math_mode='$$') pos_to_label = {} for pos, label in zip(xticks, xticklabels): if pos.is_integer(): pos_to_label[int(pos)] = label pos_to_label[pos] = label lines = {'horizontal': e_fermi, 'vertical': xticks} limits = {'y': (plot_data.min('energy'), plot_data.max('energy'))} plot_params.set_defaults(default_type='function', straight_lines=lines, x_ticks=xticks, x_ticklabels_overwrite=pos_to_label, figure_kwargs={ 'width': 1280, 'height': 720 }, x_range_padding=0.0, y_range_padding=0.0, limits=limits, color_palette='Plasma256', legend_label='Spectral function', straight_line_options={'line_color': 'white'}) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, xlabel, ylabel, figure=figure) entry = plot_data.values(first=True) plot_kw = plot_params.plot_kwargs(plot_type='image') min_energy = plot_data.min('energy') dh = plot_data.max('energy') - min_energy p.image([entry.spectral_function], x=0, y=plot_data.min('energy'), dh=dh, dw=plot_data.max('kpath'), **plot_kw, **kwargs) plot_params.draw_straight_lines(p) plot_params.set_limits(p) plot_params.set_legend(p) plot_params.save_plot(p, saveas) return p
#################################################################################################### ##################################### special plots ################################################ ####################################################################################################
[docs]@ensure_plotter_consistency(plot_params) def periodic_table_plot( values, positions=None, *, color_data=None, log_scale=False, color_map=None, data=None, copy_data=False, title='', saveas='periodictable.html', blank_outsiders='both', #min, max or both, None blank_color='#c4c4c4', include_legend=True, figure=None, **kwargs): """ Plot function for an interactive periodic table plot. Heat map and hover tool. source must be a pandas dataframe containing, atom period and group, atomic number and symbol :param values: data for the text inside each elements box :param positions: y positions relative to the middle of the box for each value :param color_data: data to display as a heatmap :param color_map: color palette to use for the heatmap (default matplotlib plasma) :param log_scale: bool, if True the heatmap is done logarithmically :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param title: str, Title of the plot :param saveas: str, filename for the saved plot :param blank_outsiders: either 'both', 'min', 'max' or None, determines, which points outside the color range to color with a default blank color :param blank_color: color to replace values outside the color range by :param include_legend: if True an additional entry with labels explaing each value entry is added :param figure: bokeh figure (optional), if provided the plot will be added to this figure Additional kwargs are passed on to the label creation for the element box The kwargs `legend_options` and `colorbar_options` can be used to overwrite default values for these regions of the plot """ from matplotlib.colors import Normalize, LogNorm from matplotlib.cm import ScalarMappable from matplotlib.cm import plasma #pylint: disable=no-name-in-module from bokeh.transform import dodge, linear_cmap, log_cmap from bokeh.sampledata.periodic_table import elements from bokeh.models import Label, ColorBar, OpenHead, Arrow, BasicTicker from bokeh.models import ColumnDataSource if isinstance(values, (dict, pd.DataFrame, ColumnDataSource)) or values is None: warnings.warn( 'Passing the dataframe as first argument is deprecated. Please pass in source by the keyword data' 'and values and positions as the first arguments', DeprecationWarning) data = values values = kwargs.pop('display_values', []) positions = kwargs.pop('display_positions', []) if 'color_value' in kwargs: warnings.warn('color_value is deprecated. Use color_data instead', DeprecationWarning) color_data = kwargs.pop('color_value') if 'outfilename' in kwargs: warnings.warn('outfilename is deprecated. Use saveas instead', DeprecationWarning) saveas = kwargs.pop('outfilename') if 'bokeh_palette' in kwargs: warnings.warn('bokeh_palette is deprecated. Use color_palette instead', DeprecationWarning) kwargs['color_palette'] = kwargs.pop('bokeh_palette') if 'copy_source' in kwargs: warnings.warn('copy_source is deprecated. Use copy_data instead', DeprecationWarning) copy_data = kwargs.pop('copy_source') if 'legend_labels' in kwargs: warnings.warn('legend_labels is deprecated. Use legend_label instead', DeprecationWarning) kwargs['legend_label'] = kwargs.pop('legend_labels') if 'color_bar_title' in kwargs: warnings.warn('color_bar_title is deprecated. Use title entry in the colorbar_options argument instead', DeprecationWarning) kwargs.setdefault('colorbar_options', {})['title'] = kwargs.pop('color_bar_title') if 'value_color_range' in kwargs: warnings.warn('The value_color_range argument is deprecated. Use the color key in the limits argument instead', DeprecationWarning) kwargs.setdefault('limits', {})['color'] = kwargs.pop('value_color_range') if not isinstance(blank_outsiders, str): warnings.warn( 'The blank_outsiders argument as a list of bools is deprecated. Use min, max or both or None instead', DeprecationWarning) if all(blank_outsiders): blank_outsiders = 'both' elif blank_outsiders[0]: blank_outsiders = 'min' elif blank_outsiders[1]: blank_outsiders = 'max' else: blank_outsiders = None if color_map is None: color_map = plasma #For this plot we use the sample data from bokeh to fill in values if data is None: data = elements if positions is None: raise NotImplementedError('Not providing positions is not yet implemented') if isinstance(color_data, list): raise ValueError('Only one color data entry allowed') plot_data = process_data_arguments(data=data, copy_data=copy_data, color=color_data, values=values, forbid_split_up={'color'}) plot_params.single_plot = False plot_params.num_plots = len(plot_data) #Create two dictionary parameters for customizing the legend and colorbar plot_params.add_parameter( 'legend_options', default_val={ 'text_font_size': '13px', #Please only provide it in pixels 'arrow_line_width': 2, 'arrow_size': 4, 'arrow_length': 0.3, 'label_standoff': 0.0, }) plot_params.add_parameter('colorbar_options', default_val={ 'height': 40, 'width': 500, 'fontsize': 12, 'label_standoff': 8, 'title': plot_data.keys(first=True).color, 'scale_alpha': 1.0 }) groups = [str(x) for x in range(1, 19)] periods = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] data['period'] = [periods[x - 1] for x in data.period] plot_params.set_defaults(default_type='function', figure_kwargs={ 'width': 1475, 'height': 675, 'x_range': groups, 'y_range': list(reversed(periods)), }, color_palette='Plasma256', format_tooltips=False, tooltips=[('Name', '@name'), ('Atomic number', '@{atomic number}'), ('Atomic mass', '@{atomic mass}'), ('CPK color', '$color[hex, swatch]:CPK'), ('Electronic configuration', '@{electronic configuration}')]) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, '', '', figure=figure) color_scale = None if any(entry.color is not None for entry in plot_data.keys()): if plot_params['limits'] is not None and plot_params['limits'].get('color') is not None: min_color, max_color = plot_params['limits']['color'] else: min_color, max_color = plot_data.min('color'), plot_data.max('color') color_values = plot_data.values(first=True).color color_name = plot_data.keys(first=True).color if not log_scale: color_mapper = linear_cmap(color_name, palette=plot_params['color_palette'], low=min_color, high=max_color) norm = Normalize(vmin=min_color, vmax=max_color) else: if min_color < 0: raise ValueError(f"Entry for 'color' element '{color_data}' is negative but log-scale is selected") color_mapper = log_cmap(color_name, palette=plot_params['color_palette'], low=min_color, high=max_color) norm = LogNorm(vmin=min_color, vmax=max_color) color_scale = ScalarMappable(norm=norm, cmap=color_map).to_rgba(color_values, alpha=None) plot_params.set_defaults(default_type='function', color=color_mapper) if blank_outsiders is not None: if blank_outsiders == 'both': outsiders = np.logical_or(color_values < min_color, color_values > max_color) elif blank_outsiders == 'min': outsiders = color_values < min_color elif blank_outsiders == 'max': outsiders = color_values > max_color plot_data.mask_data(outsiders, data_key='color', replace_value=blank_color) if include_legend: # we copy the Be entry and display it with some text again at another spot be = data[3:4].copy() be['group'] = '7' data.loc[-1] = be.values[0] data.index = data.index + 1 data = data.sort_index() r = p.rect( 'group', 'period', 0.95, 0.95, source=data, fill_alpha=0.6, # legend="metal", color=plot_params['color']) plot_params.add_tooltips(p, r) text_props = {'source': data, 'text_align': 'left', 'text_baseline': 'middle'} x = dodge('group', -0.4, range=p.x_range) # The element names p.text(x=x, y=dodge('period', -0.35, range=p.y_range), text='symbol', text_font_style='bold', text_font_size='14pt', **text_props) p.text(x=dodge('group', 0.18, range=p.x_range), y=dodge('period', 0.3, range=p.y_range), text='atomic number', text_font_size='12pt', **text_props) plot_kw = plot_params.plot_kwargs() # The values displayed on the element boxes for entry, kw, position in zip(plot_data.keys(), plot_kw, positions): p.text(x=x, y=dodge('period', position, range=p.y_range), text=entry.values, text_font_size='10pt', **text_props) label = kw.pop('legend_label', entry.values) if include_legend: options = plot_params['legend_options'] arrow_length = options['arrow_length'] y_pos = 5.5 + position x_pos = 7 + options['label_standoff'] + arrow_length legend_fontsize = options['text_font_size'] fontsize_in_data = int(legend_fontsize.rstrip('px')) / plot_params['figure_kwargs']['height'] * 7 y_pos_label = y_pos - fontsize_in_data / 2 # legend # not a real legend, but selfmade text # I do not like the hardcoded positions of the legend legendlabel = Label(x=x_pos, y=y_pos_label, text=label, border_line_color='black', border_line_alpha=0.0, background_fill_color=None, background_fill_alpha=1.0, text_font_size=legend_fontsize) p.add_layout(legendlabel) legendlabelarrow = Arrow(x_start=x_pos, x_end=x_pos - arrow_length, y_start=y_pos, y_end=y_pos, line_width=options['arrow_line_width'], end=OpenHead(line_width=options['arrow_line_width'], size=options['arrow_size'])) p.add_layout(legendlabelarrow) p.yaxis.major_label_text_font_size = '22pt' p.xaxis.visible = False p.yaxis.visible = False p.outline_line_color = None p.grid.grid_line_color = None p.axis.axis_line_color = None p.axis.major_tick_line_color = None p.axis.major_label_standoff = 0 # p.legend.orientation = "horizontal" # add color bar if any(entry.color is not None for entry in plot_data.keys()): colorbar_options = plot_params['colorbar_options'].copy() cbar_fontsize = f"{colorbar_options.pop('fontsize')}pt" cbar_location = (plot_params['figure_kwargs']['width'] * 0.2, plot_params['figure_kwargs']['height'] * 0.55) color_bar = ColorBar(color_mapper=color_mapper['transform'], title_text_font_size='12pt', ticker=BasicTicker(desired_num_ticks=10), border_line_color=None, background_fill_color=None, location=cbar_location, orientation='horizontal', major_label_text_font_size=cbar_fontsize, **colorbar_options) p.add_layout(color_bar, 'center') # deactivate grid p.grid.grid_line_color = None plot_params.set_limits(p) plot_params.save_plot(p, saveas) return p
[docs]@ensure_plotter_consistency(plot_params) def plot_lattice_constant(scaling, total_energy, *, fit_data=None, data=None, figure=None, relative=True, ref_const=None, title='Equation of states', saveas='lattice_constant', copy_data=False, **kwargs): """ Plot a lattice constant versus Total energy Plot also the fit. On the x axis is the scaling, it :param scaling: arraylike, data for the scaling factor :param total_energy: arraylike, data for the total energy :param fit_data: arraylike, optional data of fitted data :param relative: bool, scaling factor given (True), or lattice constants given? :param ref_const: float (optional), or list of floats, lattice constant for scaling 1.0 :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param copy_data: bool if True the data argument will be copied :param figure: bokeh figure (optional), if provided the plot will be added to this figure Function specific parameters: :param marker_fit: defaults to `marker`, marker type for the fit data :param marker_size_fit: defaults to `marker_size`, markersize for the fit data :param line_width_fit: defaults to `line_width`, linewidth for the fit data :param legend_label_fit: str label for the fit data Other Kwargs will be passed on to :py:func:`bokeh_line()` """ # TODO: make box which shows fit results. (fit resuls have to be past) plot_data = process_data_arguments(data=data, scaling=scaling, energy=total_energy, fit=fit_data, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) if relative: if ref_const: xlabel = rf'Relative Volume [a/{ref_const}$\AA$]' else: xlabel = r'Relative Volume' else: xlabel = r'Volume [$\AA$]' if len(plot_data) > 1: ylabel = r'Total energy norm[0] [eV]' else: ylabel = r'Total energy [eV]' #Add custom parameters for fit plot_params.add_parameter('marker_fit', default_from='marker') plot_params.add_parameter('marker_size_fit', default_from='marker_size') plot_params.add_parameter('line_width_fit', default_from='line_width') plot_params.add_parameter('legend_label_fit') plot_params.set_defaults(default_type='function', marker_fit='square', legend_label='simulation data', legend_label_fit='fit results', color='black' if len(plot_data) == 1 else None) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title=title, xlabel=xlabel, ylabel=ylabel, figure=figure) plot_kw = plot_params.plot_kwargs(post_process=False) plot_fit_kw_line = plot_params.plot_kwargs(post_process=False, plot_type='line', line_width='line_width_fit', legend_label='legend_label_fit') plot_fit_kw_scatter = plot_params.plot_kwargs(post_process=False, plot_type='scatter', marker='marker_fit', marker_size='marker_size_fit', legend_label='legend_label_fit') plot_fit_kw = {**plot_fit_kw_line, **plot_fit_kw_scatter} with NestedPlotParameters(plot_params): p = bokeh_line(plot_data.get_keys('scaling'), plot_data.get_keys('energy'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, figure=p, show=False, save_plots=False, plot_points=True, **plot_kw, **kwargs) if any(entry.fit is not None for entry in plot_data.keys()): with NestedPlotParameters(plot_params): p = bokeh_line(plot_data.get_keys('scaling'), plot_data.get_keys('fit'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel, title=title, figure=p, show=False, save_plots=False, plot_points=True, **plot_fit_kw, **kwargs) plot_params.draw_straight_lines(p) plot_params.save_plot(p, saveas) return p
######## a 2d matrix plot ########## ######### plot convergence results plot ########
[docs]@ensure_plotter_consistency(plot_params) def plot_convergence(iteration, distance, total_energy, *, data=None, saveas_energy='energy_convergence', saveas_distance='distance_convergence', figure_energy=None, figure_distance=None, xlabel='Iteration', ylabel_energy='Total energy difference [Htr]', ylabel_distance='Distance [me/bohr^3]', title_energy='Total energy difference over scf-Iterations', title_distance='Convergence (log)', copy_data=False, drop_last_iteration=False, **kwargs): """ Plot the total energy differences versus the scf iteration and plot the distance of the density versus iterations. :param iteration: data for the number of iterations :param distance: data of distances :param total_energy: data of total energies :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param xlabel: str, label for the x-axis of both plots :param saveas_energy: str, filename for the energy convergence plot :param figure_energy: Axes object for the energy convergence plot :param title_energy: str, title for the energy convergence plot :param ylabel_energy: str, label for the y-axis for the energy convergence plot :param saveas_distance: str, filename for the distance plot :param figure_distance: Axes object for the distance plot :param title_distance: str, title for the distance plot :param ylabel_distance: str, label for the y-axis for the distance plot :param copy_data: bool if True the data argument is copied :param drop_last_iteration: bool if True the last iteration is dropped for the distance plot Other Kwargs will be passed on to all :py:func:`bokeh_line()` calls """ plot_data = process_data_arguments(data=data, iteration=iteration, distance=distance, energy=total_energy, copy_data=copy_data, use_column_source=True) plot_params.single_plot = False plot_params.num_plots = len(plot_data) #Calculate energy differences and corresponding plot_data.copy_data('energy', 'energy_diff') plot_data.copy_data('iteration', 'iteration_energy') plot_data.apply('energy_diff', np.diff) plot_data.apply('energy_diff', np.abs) plot_data.apply('iteration_energy', np.delete, obj=0) plot_data.apply('iteration_energy', np.append, values=np.nan) plot_data.apply('energy_diff', np.append, values=np.nan) if drop_last_iteration: plot_data.apply('iteration', np.delete, obj=-1) if len(plot_data) == 1: default_energy_label = 'delta total energy' default_distance_label = 'distance' else: default_energy_label = [f'delta total energy {i}' for i in range(len(plot_data))] default_distance_label = [f'distance {i}' for i in range(len(plot_data))] plot_params.set_defaults(default_type='function', legend_label=default_energy_label, color='black' if len(plot_data) == 1 else None, tooltips=[('Calculation id', '$name'), ('Iteration', '@{x}'), ('Total energy difference', '@{y}')], figure_kwargs={ 'width': 800, 'height': 450, 'y_axis_type': 'log', 'x_axis_type': 'linear', }, legend_outside_plot_area=True) with NestedPlotParameters(plot_params): p1 = bokeh_line(plot_data.get_keys('iteration_energy'), plot_data.get_keys('energy_diff'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel_energy, title=title_energy, saveas=saveas_energy, figure=figure_energy, plot_points=True, set_default_legend=False, **kwargs) plot_params.set_defaults(default_type='function', legend_label=default_distance_label, tooltips=[('Calculation id', '$name'), ('Iteration', '@{x}'), ('Charge distance', '@{y}')]) with NestedPlotParameters(plot_params): p2 = bokeh_line(plot_data.get_keys('iteration'), plot_data.get_keys('distance'), data=plot_data.data, xlabel=xlabel, ylabel=ylabel_distance, title=title_distance, saveas=saveas_distance, figure=figure_distance, plot_points=True, set_default_legend=False, **kwargs) return p1, p2
[docs]@ensure_plotter_consistency(plot_params) def plot_convergence_results(iteration, distance, total_energy, *, saveas='convergence', **kwargs): """ Plot the total energy versus the scf iteration and plot the distance of the density versus iterations. Uses bokeh_line and bokeh_scatter :param iteration: list of Int :param distance: list of floats :total_energy: list of floats :param show: bool, if True call show Kwargs will be passed on to :py:func:`bokeh_line()` :returns grid: bokeh grid with figures """ from bokeh.layouts import gridplot warnings.warn( 'plot_convergence_results is deprecated. Use the more general plot_convergence instead.' 'It can do both single and multiple calculations natively', DeprecationWarning) if 'show' in kwargs: plot_params.set_parameters(show=kwargs.pop('show')) if 'save_plots' in kwargs: plot_params.set_parameters(save_plots=kwargs.pop('save_plots')) with NestedPlotParameters(plot_params): p1, p2 = plot_convergence(iteration, distance, total_energy, save_plots=False, show=False, **kwargs) grid = gridplot([p1, p2], ncols=1) plot_params.save_plot(grid, saveas) return grid
[docs]@ensure_plotter_consistency(plot_params) def plot_convergence_results_m(iterations, distances, total_energies, *, link=False, nodes=None, modes=None, plot_label=None, saveas='convergence', **kwargs): """ Plot the total energy versus the scf iteration and plot the distance of the density versus iterations in a bokeh grid for several SCF results. :param distances: list of lists of floats :total_energies: list of lists of floats :param iterations: list of lists of Int :param link: bool, optional default=False: :param nodes: list of node uuids or pks important for links :param saveas1: str, optional default='t_energy_convergence', save first figure as :param saveas2: str, optional default='distance_convergence', save second figure as :param figure_kwargs: dict, optional default={'width': 600, 'height': 450}, gets parsed to bokeh_line :param kwargs: further key-word arguments for bokeh_line :returns grid: bokeh grid with figures """ from bokeh.layouts import gridplot warnings.warn( 'plot_convergence_results_m is deprecated. Use the more general plot_convergence instead.' 'It can do both single and multiple calculations natively', DeprecationWarning) if 'show' in kwargs: plot_params.set_parameters(show=kwargs.pop('show')) if 'save_plots' in kwargs: plot_params.set_parameters(save_plots=kwargs.pop('save_plots')) if plot_label is not None: kwargs['legend_label'] = plot_label if modes is None: modes = [] with NestedPlotParameters(plot_params): p1, p2 = plot_convergence(iterations, distances, total_energies, save_plots=False, show=False, drop_last_iteration=any(mode == 'force' for mode in modes), **kwargs) grid = gridplot([p1, p2], ncols=1) plot_params.save_plot(grid, saveas) return grid
[docs]@ensure_plotter_consistency(plot_params) def matrix_plot( text_values, x_axis_data, y_axis_data, positions=None, *, color_data=None, secondary_color_data=None, x_offset=-0.47, log_scale=False, color_map=None, data=None, copy_data=False, title='', xlabel='x', ylabel='y', saveas='matrix_plot.html', blank_outsiders='both', #min, max or both, None blank_color='#c4c4c4', figure=None, categorical_axis=False, categorical_sort_key=None, block_size=0.95, block_size_pixel=100, **kwargs): """ Plot function for an interactive periodic table plot. Heat map and hover tool. source must be a pandas dataframe containing, atom period and group, atomic number and symbol :param values: data for the text inside each elements box :param positions: y positions relative to the middle of the box for each value :param color_data: data to display as a heatmap :param color_map: color palette to use for the heatmap (default matplotlib plasma) :param log_scale: bool, if True the heatmap is done logarithmically :param data: source for the data of the plot (optional) (pandas Dataframe for example) :param title: str, Title of the plot :param saveas: str, filename for the saved plot :param blank_outsiders: either 'both', 'min', 'max' or None, determines, which points outside the color range to color with a default blank color :param blank_color: color to replace values outside the color range by :param include_legend: if True an additional entry with labels explaing each value entry is added :param figure: bokeh figure (optional), if provided the plot will be added to this figure Additional kwargs are passed on to the label creation for the element box The kwargs `legend_options` and `colorbar_options` can be used to overwrite default values for these regions of the plot """ from matplotlib.cm import plasma #pylint: disable=no-name-in-module from bokeh.transform import dodge, linear_cmap, log_cmap from bokeh.models import FactorRange, ColorBar, BasicTicker if color_map is None: color_map = plasma if positions is None: raise NotImplementedError('Not providing positions is not yet implemented') plot_data = process_data_arguments(data=data, copy_data=copy_data, color=color_data, secondary_color=secondary_color_data, text=text_values, x_axis=x_axis_data, y_axis=y_axis_data, forbid_split_up={'color', 'secondary_color', 'x_axis', 'y_axis'}) plot_params.single_plot = False plot_params.num_plots = len(plot_data) plot_params.add_parameter('colorbar_options', default_val={ 'fontsize': 12, 'label_standoff': 8, 'title': plot_data.keys(first=True).color, 'scale_alpha': 1.0 }) plot_params.set_defaults(default_type='function', figure_kwargs={ 'x_axis_type': 'auto', 'y_axis_type': 'auto', }, color_palette='Plasma256', format_tooltips=False, tooltips=[]) if categorical_axis: x_values = sorted(set(plot_data.values(first=True).x_axis), key=categorical_sort_key) y_values = sorted(set(plot_data.values(first=True).y_axis), key=categorical_sort_key) plot_params.set_defaults(default_type='function', figure_kwargs={ 'height': block_size_pixel * len(y_values), 'width': block_size_pixel * len(x_values), 'x_range': FactorRange(factors=x_values), 'y_range': FactorRange(factors=y_values), }) kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs) p = plot_params.prepare_figure(title, xlabel, ylabel, figure=figure) if any(entry.color is not None for entry in plot_data.keys()): if plot_params['limits'] is not None and plot_params['limits'].get('color') is not None: min_color, max_color = plot_params['limits']['color'] else: min_color, max_color = plot_data.min('color'), plot_data.max('color') if any(entry.secondary_color is not None for entry in plot_data.keys()): min_color = min(min_color, plot_data.min('secondary_color')) max_color = max(max_color, plot_data.max('secondary_color')) color_values = [plot_data.values(first=True).color] color_name = [plot_data.keys(first=True).color] if any(entry.secondary_color is not None for entry in plot_data.keys()): color_values += [plot_data.values(first=True).secondary_color] color_name += [plot_data.keys(first=True).secondary_color] color_mappers = [] for name, value in zip(color_name, color_values): if not log_scale: color_mappers.append( linear_cmap(name, palette=plot_params['color_palette'], low=min_color, high=max_color)) else: if min_color < 0: raise ValueError(f"Entry for 'color' element '{color_data}' is negative but log-scale is selected") color_mappers.append(log_cmap(name, palette=plot_params['color_palette'], low=min_color, high=max_color)) if blank_outsiders is not None: if blank_outsiders == 'both': outsiders = np.logical_or(value < min_color, value > max_color) elif blank_outsiders == 'min': outsiders = value < min_color elif blank_outsiders == 'max': outsiders = value > max_color plot_data.mask_data(outsiders, data_key='color', replace_value=blank_color) plot_params.set_defaults(default_type='function', color=color_mappers) entry, source = plot_data.items(first=True) if any(entry.secondary_color is not None for entry in plot_data.keys()): #Explanation of what is happening here: #For plotting two colors the plan is to split up the rectangle at the #upwards diagonal and coloring each side with one color #Unfortunately there is no direct glyph to do this so we need to use the #generic patches method that needs vertices for polygons to draw #Here we define a custom Transform that takes the center point of the rectangle #and it's size and spits out a list of lists with the coordinates either x or y #and for either the upper/lower triangle #the strings in the function are the bodies of javascript functions that are inserted into the #bokeh framework via CustomJSTransform model #x/xs refers to the actual data passed in (defined by bokeh) #and all other arguments are defined in arg_dict from bokeh.models import CustomJSTransform, Dodge from bokeh.transform import transform def TriangleTransform(size, data_range, xdata=True, upper=False): """Performs a transformation from center points and a block size to triangle coordinates to divide a rectangle around this point along the upwards diagonal.""" #single value transformation transform_func = """ var x_neg = dodge_neg.compute(x) var x_pos = dodge_pos.compute(x) if (xdata && upper || !xdata && !upper) { res = [x_neg, x_pos, x_neg]; } else { res = [x_neg, x_pos, x_pos]; } return res """ #vectorized transformation (for array data) transform_v_func = """ const zip= rows=>Array.from(rows[0]).map((_,c)=> rows.map(row=>row[c])); var res; var x_neg = dodge_neg.v_compute(xs); var x_pos = dodge_pos.v_compute(xs); if (xdata && upper || !xdata && !upper) { res = zip([x_neg, x_pos, x_neg]); } else { res = zip([x_neg, x_pos, x_pos]); } console.log(res); return res; """ arg_dict = { 'dodge_neg': Dodge(value=-size / 2, range=data_range), 'dodge_pos': Dodge(value=size / 2, range=data_range), 'xdata': xdata, 'upper': upper, } return CustomJSTransform(func=transform_func, v_func=transform_v_func, args=arg_dict) upper = p.patches(transform(entry.x_axis, TriangleTransform(block_size, p.x_range, xdata=True, upper=True)), transform(entry.y_axis, TriangleTransform(block_size, p.y_range, xdata=False, upper=True)), source=source, fill_alpha=0.6, color=plot_params[('color', 0)]) lower = p.patches(transform(entry.x_axis, TriangleTransform(block_size, p.x_range, xdata=True, upper=False)), transform(entry.y_axis, TriangleTransform(block_size, p.y_range, xdata=False, upper=False)), source=source, fill_alpha=0.6, color=plot_params[('color', 1)]) r = [upper, lower] else: r = p.rect(entry.x_axis, entry.y_axis, block_size, block_size, source=source, fill_alpha=0.6, color=plot_params[('color', 0)]) plot_params.add_tooltips(p, r) plot_kw = plot_params.plot_kwargs(plot_type='text', ignore='color') # The values displayed on the element boxes for (entry, source), kw, position in zip(plot_data.items(), plot_kw, positions): p.text(x=dodge(entry.x_axis, x_offset, range=p.x_range), y=dodge(entry.y_axis, position, range=p.y_range), text=entry.text, source=source, **kw) # add color bar if any(entry.color is not None for entry in plot_data.keys()): colorbar_options = plot_params['colorbar_options'].copy() cbar_fontsize = f"{colorbar_options.pop('fontsize')}pt" color_bar = ColorBar(color_mapper=color_mappers[0]['transform'], title_text_font_size='12pt', ticker=BasicTicker(desired_num_ticks=10), border_line_color=None, background_fill_color=None, orientation='vertical', major_label_text_font_size=cbar_fontsize, **colorbar_options) p.add_layout(color_bar, 'right') # deactivate grid p.grid.grid_line_color = None plot_params.set_limits(p) plot_params.save_plot(p, saveas) return p