def mv_grid_topology(pypsa_network, configs, timestep=None,
                     line_color=None, node_color=None,
                     line_load=None, grid_expansion_costs=None,
                     filename=None, arrows=False,
                     grid_district_geom=True, background_map=True,
                     voltage=None, limits_cb_lines=None, limits_cb_nodes=None,
                     xlim=None, ylim=None, lines_cmap='inferno_r',
                     title='', scaling_factor_line_width=None):
    """
    Plot line loading as color on lines.

    Displays line loading relative to nominal capacity.

    Parameters
    ----------
    pypsa_network : :pypsa:`pypsa.Network<network>`
    configs : :obj:`dict`
        Dictionary with used configurations from config files. See
        :class:`~.grid.network.Config` for more information.
    timestep : :pandas:`pandas.Timestamp<timestamp>`
        Time step to plot analysis results for. If `timestep` is None maximum
        line load and if given, maximum voltage deviation, is used. In that
        case arrows cannot be drawn. Default: None.
    line_color : :obj:`str`
        Defines whereby to choose line colors (and implicitly size). Possible
        options are:

        * 'loading'
          Line color is set according to loading of the line. Loading of MV
          lines must be provided by parameter `line_load`.
        * 'expansion_costs'
          Line color is set according to investment costs of the line. This
          option also effects node colors and sizes by plotting investment in
          stations and setting `node_color` to 'storage_integration' in order
          to plot storage size of integrated storages. Grid expansion costs
          must be provided by parameter `grid_expansion_costs`.
        * None (default)
          Lines are plotted in black. Is also the fallback option in case of
          wrong input.

    node_color : :obj:`str`
        Defines whereby to choose node colors (and implicitly size). Possible
        options are:

        * 'technology'
          Node color as well as size is set according to type of node
          (generator, MV station, etc.).
        * 'voltage'
          Node color is set according to voltage deviation from 1 p.u..
          Voltages of nodes in MV grid must be provided by parameter `voltage`.
        * 'storage_integration'
          Only storages are plotted. Size of node corresponds to size of
          storage.
        * None (default)
          Nodes are not plotted. Is also the fallback option in case of wrong
          input.

    line_load : :pandas:`pandas.DataFrame<dataframe>`
        Dataframe with current results from power flow analysis in A. Index of
        the dataframe is a :pandas:`pandas.DatetimeIndex<datetimeindex>`,
        columns are the line representatives.
    grid_expansion_costs : :pandas:`pandas.DataFrame<dataframe>`
        Dataframe with grid expansion costs in kEUR. See `grid_expansion_costs`
        in :class:`~.grid.network.Results` for more information.
    filename : :obj:`str`
        Filename to save plot under. If not provided, figure is shown directly.
        Default: None.
    arrows : :obj:`Boolean`
        If True draws arrows on lines in the direction of the power flow. Does
        only work when `line_color` option 'loading' is used and a time step
        is given.
        Default: False.
    limits_cb_lines : :obj:`tuple`
        Tuple with limits for colorbar of line color. First entry is the
        minimum and second entry the maximum value. Default: None.
    limits_cb_nodes : :obj:`tuple`
        Tuple with limits for colorbar of nodes. First entry is the
        minimum and second entry the maximum value. Default: None.
    xlim : :obj:`tuple`
        Limits of x-axis. Default: None.
    ylim : :obj:`tuple`
        Limits of y-axis. Default: None.
    lines_cmap : :obj:`str`
        Colormap to use for lines in case `line_color` is 'loading' or
        'expansion_costs'. Default: 'inferno_r'.
    title : :obj:`str`
        Title of the plot. Default: ''.
    scaling_factor_line_width : :obj:`float` or None
        If provided line width is set according to the nominal apparent power
        of the lines. If line width is None a default line width of 2 is used
        for each line. Default: None.

    """

    def get_color_and_size(name, colors_dict, sizes_dict):
        if 'BranchTee' in name:
            return colors_dict['BranchTee'], sizes_dict['BranchTee']
        elif 'LVStation' in name:
            return colors_dict['LVStation'], sizes_dict['LVStation']
        elif 'GeneratorFluctuating' in name:
            return (colors_dict['GeneratorFluctuating'],
                    sizes_dict['GeneratorFluctuating'])
        elif 'Generator' in name:
            return colors_dict['Generator'], sizes_dict['Generator']
        elif 'DisconnectingPoint' in name:
            return (colors_dict['DisconnectingPoint'],
                    sizes_dict['DisconnectingPoint'])
        elif 'MVStation' in name:
            return colors_dict['MVStation'], sizes_dict['MVStation']
        elif 'Storage' in name:
            return colors_dict['Storage'], sizes_dict['Storage']
        else:
            return colors_dict['else'], sizes_dict['else']

    def nodes_by_technology(buses):
        bus_sizes = {}
        bus_colors = {}
        colors_dict = {'BranchTee': 'b',
                       'GeneratorFluctuating': 'g',
                       'Generator': 'k',
                       'LVStation': 'c',
                       'MVStation': 'r',
                       'Storage': 'y',
                       'DisconnectingPoint': '0.75',
                       'else': 'orange'}
        sizes_dict = {'BranchTee': 10,
                      'GeneratorFluctuating': 100,
                      'Generator': 100,
                      'LVStation': 50,
                      'MVStation': 120,
                      'Storage': 100,
                      'DisconnectingPoint': 50,
                      'else': 200}
        for bus in buses:
            bus_colors[bus], bus_sizes[bus] = get_color_and_size(
                bus, colors_dict, sizes_dict)
        return bus_sizes, bus_colors

    def nodes_by_voltage(buses, voltage):
        bus_colors = {}
        bus_sizes = {}
        for bus in buses:
            if 'primary' in bus:
                bus_tmp = bus[12:]
            else:
                bus_tmp = bus[4:]
            if timestep is not None:
                bus_colors[bus] = 100 * abs(1 - voltage.loc[timestep,
                                                      ('mv', bus_tmp)])
            else:
                bus_colors[bus] = 100 * max(abs(1 - voltage.loc[
                                                    :, ('mv', bus_tmp)]))
            bus_sizes[bus] = 50
        return bus_sizes, bus_colors

    def nodes_storage_integration(buses):
        bus_sizes = {}
        for bus in buses:
            if not 'storage' in bus:
                bus_sizes[bus] = 0
            else:
                tmp = bus.split('_')
                storage_repr = '_'.join(tmp[1:])
                bus_sizes[bus] = pypsa_network.storage_units.loc[
                           storage_repr, 'p_nom'] * 1000 / 3
        return bus_sizes

    def nodes_by_costs(buses, grid_expansion_costs):
        # sum costs for each station
        costs_lv_stations = grid_expansion_costs[
            grid_expansion_costs.index.str.contains("LVStation")]
        costs_lv_stations['station'] = \
            costs_lv_stations.reset_index()['index'].apply(
                lambda _: '_'.join(_.split('_')[0:2])).values
        costs_lv_stations = costs_lv_stations.groupby('station').sum()
        costs_mv_station = grid_expansion_costs[
            grid_expansion_costs.index.str.contains("MVStation")]
        costs_mv_station['station'] = \
            costs_mv_station.reset_index()['index'].apply(
                lambda _: '_'.join(_.split('_')[0:2])).values
        costs_mv_station = costs_mv_station.groupby('station').sum()

        bus_sizes = {}
        bus_colors = {}
        for bus in buses:
            if 'LVStation' in bus:
                try:
                    tmp = bus.split('_')
                    lv_st = '_'.join(tmp[2:])
                    bus_colors[bus] = costs_lv_stations.loc[
                        lv_st, 'total_costs']
                    bus_sizes[bus] = 100
                except:
                    bus_colors[bus] = 0
                    bus_sizes[bus] = 0
            elif 'MVStation' in bus:
                try:
                    tmp = bus.split('_')
                    mv_st = '_'.join(tmp[2:])
                    bus_colors[bus] = costs_mv_station.loc[
                        mv_st, 'total_costs']
                    bus_sizes[bus] = 100
                except:
                    bus_colors[bus] = 0
                    bus_sizes[bus] = 0
            else:
                bus_colors[bus] = 0
                bus_sizes[bus] = 0

        return bus_sizes, bus_colors

    # set font and font size
    font = {'family': 'serif',
            'size': 15}
    matplotlib.rc('font', **font)

    # create pypsa network only containing MV buses and lines
    pypsa_plot = PyPSANetwork()
    pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom >= 10]
    # filter buses of aggregated loads and generators
    pypsa_plot.buses = pypsa_plot.buses[
        ~pypsa_plot.buses.index.str.contains("agg")]
    pypsa_plot.lines = pypsa_network.lines[
        pypsa_network.lines.bus0.isin(pypsa_plot.buses.index)][
        pypsa_network.lines.bus1.isin(pypsa_plot.buses.index)]

    # line colors
    if line_color == 'loading':
        # calculate relative line loading
        # get load factor
        residual_load = tools.get_residual_load_from_pypsa_network(
            pypsa_network)
        case = residual_load.apply(
                lambda _: 'feedin_case' if _ < 0 else 'load_case')
        if timestep is not None:
            timeindex = [timestep]
        else:
            timeindex = line_load.index
        load_factor = pd.DataFrame(
            data={'s_nom': [float(configs[
                                      'grid_expansion_load_factors'][
                                      'mv_{}_line'.format(case.loc[_])])
                            for _ in timeindex]},
            index=timeindex)
        # get allowed line load
        s_allowed = load_factor.dot(
            pypsa_plot.lines.s_nom.to_frame().T * 1e3)
        # get line load from pf
        line_colors = line_load.loc[:, pypsa_plot.lines.index].divide(
            s_allowed).max()
    elif line_color == 'expansion_costs':
        node_color = 'expansion_costs'
        line_costs = pypsa_plot.lines.join(
            grid_expansion_costs, rsuffix='costs', how='left')
        line_colors = line_costs.total_costs.fillna(0)
    else:
        line_colors = pd.Series('black', index=pypsa_plot.lines.index)

    # bus colors and sizes
    if node_color == 'technology':
        bus_sizes, bus_colors = nodes_by_technology(pypsa_plot.buses.index)
        bus_cmap = None
    elif node_color == 'voltage':
        bus_sizes, bus_colors = nodes_by_voltage(
            pypsa_plot.buses.index, voltage)
        bus_cmap = plt.cm.Blues
    elif node_color == 'storage_integration':
        bus_sizes = nodes_storage_integration(pypsa_plot.buses.index)
        bus_colors = 'orangered'
        bus_cmap = None
    elif node_color == 'expansion_costs':
        bus_sizes, bus_colors = nodes_by_costs(pypsa_plot.buses.index,
                                               grid_expansion_costs)
        bus_cmap = None
    elif node_color is None:
        bus_sizes = 0
        bus_colors = 'r'
        bus_cmap = None
    else:
        logging.warning('Choice for `node_color` is not valid. Default is '
                        'used instead.')
        bus_sizes = 0
        bus_colors = 'r'
        bus_cmap = None

    # convert bus coordinates to Mercator
    if contextily and background_map:
        inProj = Proj(init='epsg:4326')
        outProj = Proj(init='epsg:3857')
        x2, y2 = transform(inProj, outProj,
                           list(pypsa_plot.buses.loc[:, 'x']),
                           list(pypsa_plot.buses.loc[:, 'y']))
        pypsa_plot.buses.loc[:, 'x'] = x2
        pypsa_plot.buses.loc[:, 'y'] = y2

    # plot
    plt.figure(figsize=(12, 8))
    ax = plt.gca()

    # plot grid district
    if grid_district_geom and geopandas:
        try:
            subst = pypsa_network.buses[
                pypsa_network.buses.index.str.contains("MVStation")].index[0]
            subst_id = subst.split('_')[-1]
            projection = 3857 if contextily and background_map else 4326
            region = get_grid_district_polygon(configs, subst_id=subst_id,
                                               projection=projection)
            region.plot(ax=ax, color='white', alpha=0.2,
                        edgecolor='red', linewidth=2)
        except Exception as e:
            logging.warning("Grid district geometry could not be plotted due "
                            "to the following error: {}".format(e))

    # if scaling factor is given s_nom is plotted as line width
    if scaling_factor_line_width is not None:
        line_width = pypsa_plot.lines.s_nom * scaling_factor_line_width
    else:
        line_width = 2
    cmap = plt.cm.get_cmap(lines_cmap)
    ll = pypsa_plot.plot(line_colors=line_colors, line_cmap=cmap, ax=ax,
                         title=title, line_widths=line_width,
                         branch_components=['Line'], basemap=True,
                         bus_sizes=bus_sizes, bus_colors=bus_colors,
                         bus_cmap=bus_cmap)

    # color bar line loading
    if line_color == 'loading':
        if limits_cb_lines is None:
            limits_cb_lines = (min(line_colors), max(line_colors))
        v = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101)
        cb = plt.colorbar(ll[1], boundaries=v, ticks=v[0:101:10])
        cb.set_clim(vmin=limits_cb_lines[0], vmax=limits_cb_lines[1])
        cb.set_label('Line loading in p.u.')
    # color bar grid expansion costs
    elif line_color == 'expansion_costs':
        if limits_cb_lines is None:
            limits_cb_lines = (min(min(line_colors), min(bus_colors.values())),
                              max(max(line_colors), max(bus_colors.values())))
        v = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101)
        cb = plt.colorbar(ll[1], boundaries=v, ticks=v[0:101:10])
        cb.set_clim(vmin=limits_cb_lines[0], vmax=limits_cb_lines[1])
        cb.set_label('Grid expansion costs in kEUR')

    # color bar voltage
    if node_color == 'voltage':
        if limits_cb_nodes is None:
            limits_cb_nodes = (min(bus_colors.values()),
                                 max(bus_colors.values()))
        v_voltage = np.linspace(limits_cb_nodes[0], limits_cb_nodes[1], 101)
        cb_voltage = plt.colorbar(ll[0], boundaries=v_voltage,
                                  ticks=v_voltage[0:101:10])
        cb_voltage.set_clim(vmin=limits_cb_nodes[0],
                            vmax=limits_cb_nodes[1])
        cb_voltage.set_label('Voltage deviation in %')

    # storages
    if node_color == 'expansion_costs':
        ax.scatter(
            pypsa_plot.buses.loc[
                pypsa_network.storage_units.loc[:, 'bus'], 'x'],
            pypsa_plot.buses.loc[
                pypsa_network.storage_units.loc[:, 'bus'], 'y'],
            c='orangered',
            s=pypsa_network.storage_units.loc[:, 'p_nom'] * 1000 / 3)
    # add legend for storage size and line capacity
    if (node_color == 'storage_integration' or
        node_color == 'expansion_costs') and \
            pypsa_network.storage_units.loc[:, 'p_nom'].any() > 0:
        scatter_handle = plt.scatter(
            [], [], c='orangered', s=100, label='= 300 kW battery storage')
    else:
        scatter_handle = None
    if scaling_factor_line_width is not None:
        line_handle = plt.plot(
            [], [], c='black', linewidth=scaling_factor_line_width*10,
            label='= 10 MVA')
    else:
        line_handle = None
    if scatter_handle and line_handle:
        plt.legend(handles=[scatter_handle, line_handle[0]], labelspacing=1,
                   title='Storage size and line capacity', borderpad=0.5,
                   loc=2, framealpha=0.5, fontsize='medium')
    elif scatter_handle:
        plt.legend(handles=[scatter_handle], labelspacing=1,
                   title='Storage size', borderpad=0.5,
                   loc=2, framealpha=0.5, fontsize='medium')
    elif line_handle:
        plt.legend(handles=[line_handle[0]], labelspacing=1,
                   title='Line capacity', borderpad=0.5,
                   loc=2, framealpha=0.5, fontsize='medium')

    # axes limits
    if xlim is not None:
        ax.set_xlim(xlim[0], xlim[1])
    if ylim is not None:
        ax.set_ylim(ylim[0], ylim[1])

    # hide axes labels
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # draw arrows on lines
    if arrows and timestep and line_color=='loading':
        path = ll[1].get_segments()
        colors = cmap(ll[1].get_array() / 100)
        for i in range(len(path)):
            if pypsa_network.lines_t.p0.loc[timestep,
                                            line_colors.index[i]] > 0:
                arrowprops = dict(arrowstyle="->", color='b')#colors[i])
            else:
                arrowprops = dict(arrowstyle="<-", color='b')#colors[i])
            ax.annotate(
                "",
                xy=abs(
                    (path[i][0] - path[i][1]) * 0.51 - path[i][0]),
                xytext=abs(
                    (path[i][0] - path[i][1]) * 0.49 - path[i][0]),
                arrowprops=arrowprops,
                size=10)

    # plot map data in background
    if contextily and background_map:
        try:
            add_basemap(ax, zoom=12)
        except Exception as e:
            logging.warning("Background map could not be plotted due to the "
                            "following error: {}".format(e))

    if filename is None:
        plt.show()
    else:
        plt.savefig(filename)
        plt.close()