Beispiel #1
0
def bokeh_spacetimepop(
        frm,
        geometry,
        title = '',
        preamble = '',
        varNames = None,
        varNotes = dict(),
        pw = 700,
        ph = 700,
        xZones = dict(),
        ):

    import numpy as np

    import pandas as pd
    df = pd.DataFrame
    idx = pd.IndexSlice
    import geopandas as gpd
    gdf = gpd.GeoDataFrame

    from bokeh.models import ColumnDataSource, HoverTool, Legend, LegendItem, CDSView, IndexFilter
    from bokeh.plotting import figure, show
    from bokeh.io import output_notebook

    #     frm = frm.reset_index().pivot(index = frm.index.names[0], columns = frm.index.names[1])
    frm = frm.copy()
    frm = frm.sort_index()
    #     geometry = geometry.copy()

    from bokeh.models import Div

    title = f'<h1>{title}</h1>'
    title = Div(
        text = title,
        width = pw,
        )
    preamble = Div(
        text = preamble,
        width = pw,
        )

    if varNames is None:
        varNames = frm.columns.sort_values()
        varMetaName = varNames.name
    else:
        varMetaName = 'variable'
    varNames = list(varNames)
    seriesNames = frm.index.levels[1].sort_values()
    seriesMetaName = seriesNames.name
    seriesNames = list(seriesNames)
    dates = [str(int(round(i.to_numpy().astype(int) / 1e6))) for i in frm.index.levels[0]]
    frm.index = frm.index.set_levels(dates, level = 0)
    defaultVar = varNames[0]
    defaultDate = dates[-1]
    pivotFrm = frm.reset_index() \
        .pivot(index = frm.index.names[0], columns = frm.index.names[1]) \
        .sort_index()

    defaultVar = varNames[0]
    defaultDate = dates[-1]

    for key in varNames:
        if not key in varNotes:
            varNotes[key] = ''
        else:
            varNotes[key] = f'<i>{varNotes[key]}</i>'

    varNote = Div(
        text = varNotes[defaultVar],
        width = pw - 120,
        )

    lineSources = {
        key: ColumnDataSource(pivotFrm[key])
            for key in pivotFrm.columns.levels[0]
        }
    lineSource = ColumnDataSource(pivotFrm[defaultVar])
    lineSource.name = defaultVar

    barSources = dict()
    for varName in varNames:
        for index, date in zip(sorted(pivotFrm.index), dates):
            series = pivotFrm.loc[index, varName]
            subFrm = df(dict(
                name = series.index,
                value = series.values,
                height = abs(series.values),
                offset = series.values / 2.
                ))
            barSources[varName + '_' + date] = ColumnDataSource(subFrm)
    barSource = ColumnDataSource(barSources[defaultVar + '_' + defaultDate].data)
    barSource.name = ', '.join([str(defaultVar), str(defaultDate)])

    bounds = geometry.bounds
    minx = np.min(bounds['minx'])
    maxx = np.max(bounds['maxx'])
    miny = np.min(bounds['miny'])
    maxy = np.max(bounds['maxy'])
    aspect = (maxx - minx) / (maxy - miny)
    from shapely.geometry import Polygon
    import itertools
    corners = list(itertools.product(geometry.total_bounds[::2], geometry.total_bounds[1::2]))
    allPoly = Polygon([corners[0], corners[1], corners[3], corners[2]])
    allPoly = allPoly.centroid.buffer(np.sqrt(allPoly.area) / 1e6)
    for name in frm.index.levels[1]:
        if not name in geometry.index:
            geometry[name] = allPoly
    geometry = geometry.simplify(np.sqrt(geometry.area).min() * 10. ** 3.5)
    geoFrm = frm.reset_index().pivot(index = frm.index.names[1], columns = frm.index.names[0])
    geoFrm.columns = geoFrm.columns.map('_'.join).str.strip('_')
    geoFrm['geometry'] = geometry
    geoFrm = gdf(geoFrm)
    from bokeh.models import GeoJSONDataSource
    geoJSON = geoFrm.reset_index().to_json()
    geoSource = GeoJSONDataSource(geojson = geoJSON)
    mins = {n: frm[n].min() for n in varNames}
    maxs = {n: frm[n].max() for n in varNames}

    xName = frm.index.names[0]

    lineFig = figure(
        x_axis_type = 'datetime',
        y_range = (mins[defaultVar], maxs[defaultVar]),
        plot_height = int((ph - 100) * 1. / 3.),
        plot_width = pw,
        toolbar_location = 'left',
        tools = 'save, xpan, box_zoom, reset, xwheel_zoom',
        active_scroll = 'auto',
    #         title = title,
        )

    barFig = figure(
        x_range = seriesNames,
        plot_height = int((ph - 100) * 1. / 2.),
        plot_width = pw,
    #         title = "Scores on my birthday",
        toolbar_location = None,
        tools = ""
        )
    barFig.xgrid.grid_line_color = None
    barFig.xaxis.major_label_orientation = 'vertical'

    mapFig = figure(
        plot_width = pw - 20,
        plot_height = int(round((pw - 20) / aspect)),
        toolbar_location = 'right',
        tools = 'pan, wheel_zoom, reset',
        background_fill_color = "lightgrey"
        )
    mapFig.xgrid.grid_line_color = None
    mapFig.ygrid.grid_line_color = None

    from matplotlib.pyplot import get_cmap
    from matplotlib.colors import rgb2hex
    cmap = get_cmap('nipy_spectral')
    cs = [rgb2hex(cmap(i / len(seriesNames), alpha = 0.5)) for i in range(len(seriesNames))]

    lines = []

    for seriesName, colour in zip(seriesNames, cs):

        line = lineFig.line(
            xName,
            seriesName,
            source = lineSource,
            color = colour,
            alpha = 0.8,
            muted_color = 'gray',
            muted_alpha = 0.3,
            muted = True,
            line_width = 2,
    #             legend_label = seriesName,
            )

        from bokeh.models import HoverTool
        lineFig.add_tools(HoverTool(
            renderers = [
                line,
                ],
            tooltips = [
                (seriesMetaName.capitalize(), seriesName),
                (xName.capitalize(), f'@{xName}' + '{%Y-%m-%d}'),
                ('Value', f'@{{{seriesName}}}'),
                ],
            formatters = {
                f'@{xName}': 'datetime',
                seriesName: 'numeral',
                },
            toggleable = False
            ))

        lines.append(line)

    bars = []
    for i, (seriesName, colour) in enumerate(zip(seriesNames, cs)):
        view = CDSView(source = barSource, filters = [IndexFilter([i,]),])
        bar = barFig.rect(
            source = barSource,
            view = view,
            x = 'name',
            y = 'offset',
            height = 'height',
            width = 0.9,
            color = colour,
            muted_color = 'gray',
            muted_alpha = 0.3,
            muted = True,
            )
        bars.append(bar)

    from bokeh.palettes import Viridis256
    from bokeh.models import LinearColorMapper, ColorBar
    palette = Viridis256
    mapColourMapper = LinearColorMapper(
        palette = palette,
        low = frm.loc[idx[defaultDate, :], defaultVar].min(),
        high = frm.loc[idx[defaultDate, :], defaultVar].max(),
        )
    mapColourBar = ColorBar(
        color_mapper = mapColourMapper, 
        label_standoff = 8,
        width = 30,
        height = int(round(mapFig.plot_height * 0.9)),
        border_line_color = None,
        location = (0, 0), 
        orientation = 'vertical',
        )
    mapFig.add_layout(mapColourBar, 'left')

    patches = []
    for i, seriesName in enumerate(seriesNames):
        view = CDSView(source = geoSource, filters = [IndexFilter([i,]),])
        patch = mapFig.patches(
            'xs',
            'ys',
            source = geoSource,
            view = view,
            fill_color = dict(
                field = '_'.join([defaultVar, defaultDate]),
                transform = mapColourMapper,
                ),
            line_color = 'grey', 
            line_width = 0.25,
            fill_alpha = 0.,
            name = '_'.join([defaultVar, defaultDate])
            )
        patches.append(patch)

    from bokeh.models import HoverTool
    mapHover = HoverTool(
        renderers = patches,
        tooltips = [
            (seriesMetaName.capitalize(), f'@{seriesMetaName}'),
            ('Value', '@$name'),
            ]
        )
    mapFig.add_tools(mapHover)

    from bokeh.models import BoxAnnotation
    from bokeh.models import Label
    for name, zone in xZones.items():
        convD = lambda x: int(round(pd.Timestamp(x).to_numpy().astype(int) / 1e6))
        left, right = [None if val is None else convD(val) for val in zone]
        zone = BoxAnnotation(
            left = left,
            right = right,
            fill_alpha = 0.1,
            fill_color = 'gray',
            )
        zoneLabel = Label(
            text = name + ' (end)' if left is None else name,
            text_font_size = '8pt',
            x = right if left is None else left,
            y = 10,
            x_units = 'data',
            y_units = 'screen',
            angle = -90 if left is None else 90,
            angle_units = 'deg',
            x_offset = -10 if left is None else 10,
            y_offset = 5 * (len(name) + 6) if left is None else 0
            )
        lineFig.add_layout(zone)
        lineFig.add_layout(zoneLabel)

    from bokeh.models import Span
    span = Span(
        location = int(defaultDate),
        dimension = 'height',
        line_color = 'red',
    #         line_dash = 'dashed',
        line_width = 1
        )
    lineFig.add_layout(span)

    from bokeh.models.widgets import DateSlider
    slider = DateSlider(
        title = 'Date',
        start = int(dates[0]),
        end = int(dates[-1]),
        step = int(8.64 * 1e7), # days
        value = int(defaultDate),
        width = pw - 60,
        align = 'end'
        )

    from bokeh.models.widgets import Select
    select = Select(
        title = "Choose data:",
        options = varNames,
        value = defaultVar,
        width = 100,
        )

    from bokeh.models import CheckboxGroup
    checkboxes = CheckboxGroup(
        labels = seriesNames,
        active = [],
        )
    checkboxAll = CheckboxGroup(
        labels = ['All',],
        active = [],
        )

    from bokeh.models import CustomJS
    callback = CustomJS(
        args = dict(
            y_range = lineFig.y_range,
            lineSources = lineSources,
            lineSource = lineSource,
            barSources = barSources,
            barSource = barSource,
            bars = bars,
            lines = lines,
            patches = patches,
            select = select,
            slider = slider,
            span = span,
            checkboxes = checkboxes,
            varNote = varNote,
            varNotes = varNotes,
            geoSource = geoSource,
            mapColourMapper = mapColourMapper,
            mins = mins,
            maxs = maxs,
            ),
        code = """
            lineSource.data = lineSources[select.value].data
            lineSource.name = select.value
            lineSource.change.emit()
            span.location = slider.value
            span.change.emit()
            y_range.setv({'start': mins[select.value], 'end': maxs[select.value]})
            varNote.text = varNotes[select.value]
            varNote.change.emit()
            const barChoice = select.value + '_' + slider.value
            barSource.data = barSources[barChoice].data
            barSource.name = select.value.toString() + ', ' + slider.value.toString()
            barSource.change.emit()
            for (let i = 0; i < lines.length; i++){
                let checked = checkboxes.active.includes(i)
                lines[i].muted = !(checked)
                bars[i].muted = !(checked)
                var alpha = checked ? 1 : 0;
                patches[i].glyph.fill_alpha = alpha
            }
            const newCol = select.value + '_' + slider.value
            for (let i = 0; i < lines.length; i++){
                patches[i].glyph.fill_color['field'] = newCol
                patches[i].name = newCol
            }
            mapColourMapper.low = mins[select.value]
            mapColourMapper.high = maxs[select.value]
            geoSource.change.emit()
            """,
        )

    allCheckCallback = CustomJS(
        args = dict(
            lines = lines,
            checkboxes = checkboxes,
            checkboxAll = checkboxAll,
            callback = callback
            ),
        code = """
            checkboxes.active.length = 0
            if (checkboxAll.active.length > 0) {
                let arr = []
                for (let i = 0; i < lines.length; i++){
                    arr.push(i)
                    }
                checkboxes.active.push(...arr)
            }
            checkboxes.change.emit()
            callback.execute()
            """
        )

    slider.js_on_change('value', callback)
    select.js_on_change('value', callback)
    checkboxes.js_on_change('active', callback)
    checkboxAll.js_on_change('active', allCheckCallback)

    from bokeh.layouts import column, row
    layout = column(
        title,
        preamble,
        row(select, varNote),
        row(column(lineFig, slider, barFig), column(checkboxes, checkboxAll)),
        mapFig
        )

    return layout
Beispiel #2
0
    plots[0].x_range.end=len;
    plots[0].y_range.end=closest;
    plots[0].y_range.start=0;
    plots[1].x_range.end=len;
    plots[1].y_range.end=closest;
    plots[1].y_range.start=1;
''')
#changes x range to be (0,slider_value)
#changes y range to be (0 or 1, the closest stastics value to slider)

slider = DateSlider(start=min(dates),
                    end=max(dates),
                    value=max(dates),
                    step=1,
                    title='Time')
slider.js_on_change('value', callback)

loopchange = CustomJS(args=dict(slider=slider,
                                endlen=max(dates),
                                minlen=min(dates)),
                      code='''
    function sleep(ms) {
       return new Promise(resolve => setTimeout(resolve, ms));
    }
    async function run() {
        var len = slider.value;
        var loopcount=0;
        while (cb_obj.active){
            if (len>=endlen){
                loopcount++;
                if(loopcount==2){
Beispiel #3
0
def make_dateMap(frm, name, title, size = 600, nonVisKeys = {}):

    minx = np.min(frm.bounds['minx'])
    maxx = np.max(frm.bounds['maxx'])
    miny = np.min(frm.bounds['miny'])
    maxy = np.max(frm.bounds['maxy'])
    aspect = (maxx - minx) / (maxy - miny)

    ts = sorted(set([n.split('_')[-1] for n in frm.columns]))
    ts = [n for n in ts if n.isnumeric()]
    assert len(ts)
    ns = sorted(set([n.split('_')[0] for n in frm.columns]))
    ns = [n for n in ns if not n in [*nonVisKeys, 'geometry']]
    assert len(ns)

    defaultCol = '_'.join([ns[0], ts[-1]])

    indexName = frm.index.name

    mins = {n: frm[['_'.join([n, t]) for t in ts]].min().min() for n in ns}
    maxs = {n: frm[['_'.join([n, t]) for t in ts]].max().max() for n in ns}

    from bokeh.models import GeoJSONDataSource
    geoJSON = frm.reset_index().to_json()
    source = GeoJSONDataSource(geojson = geoJSON)

    from bokeh.io import output_file
    outFilename = name + '.html'
    outPath = os.path.join(dataDir, outFilename)
    if os.path.isfile(outPath):
        os.remove(outPath)
    output_file(outPath)

    from bokeh.plotting import figure
    fig = figure(
        title = title,
        plot_height = size,
        plot_width = int(round(size * aspect)) + 50, 
        toolbar_location = 'right',
        tools = 'pan, zoom_in, zoom_out, wheel_zoom, reset',
        background_fill_color = "lightgrey"
        )

    fig.xgrid.grid_line_color = None
    fig.ygrid.grid_line_color = None

    from bokeh.palettes import Viridis256
    from bokeh.models import LinearColorMapper, ColorBar
    palette = Viridis256
    colourMapper = LinearColorMapper(
        palette = palette,
        low = mins[ns[0]],
        high = maxs[ns[0]],
        )
    colourBar = ColorBar(
        color_mapper = colourMapper, 
        label_standoff = 8,
        width = 30,
        height = int(round(fig.plot_height * 0.9)),
        border_line_color = None,
        location = (0, 0), 
        orientation = 'vertical',
        )
    fig.add_layout(colourBar, 'left')

    patches = fig.patches(
        'xs',
        'ys',
        source = source,
        fill_color = dict(
            field = defaultCol,
            transform = colourMapper,
            ),
        line_color = 'grey', 
        line_width = 0.25,
        fill_alpha = 1,
        name = defaultCol
        )

    from bokeh.models.widgets import DateSlider as Slider
    slider = Slider(
        title = 'Date',
        start = int(ts[0]),
        end = int(ts[-1]),
        step = int(8.64 * 1e7), # days
        value = int(ts[-1]),
        width = fig.plot_width - 70
        )

    from bokeh.models.widgets import Select
    select = Select(
        title = "Dataset",
        options = ns,
        value = defaultCol.split('_')[0],
        width = 60
        )

    from bokeh.models import CustomJS
    callback = CustomJS(
        args = dict(
            patches = patches,
            source = source,
            slider = slider,
    #         key = 'stay', # <--- TESTING
            select = select,
            colourMapper = colourMapper,
            mins = mins,
            maxs = maxs,
            ),
        code = """
            const newCol = select.value + '_' + slider.value
            patches.glyph.fill_color['field'] = newCol
            patches.name = newCol
            colourMapper.low = mins[select.value]
            colourMapper.high = maxs[select.value]
            source.change.emit()
            """,
        )

    from bokeh.models import HoverTool
    tooltips = [
        ('Index', '@' + indexName),
        ('Value', '@$name')
        ]
    tooltips.extend([(k.capitalize(), '@' + k) for k in nonVisKeys])
    hover = HoverTool(
        renderers = [patches],
        tooltips = tooltips
        )
    fig.add_tools(hover)

    slider.js_on_change('value', callback)
    select.js_on_change('value', callback)

    from bokeh.layouts import column, row
    layout = column(fig, row(select, slider))

    from bokeh.io import show

    show(layout)