facultyai / dash-bootstrap-components

Bootstrap components for Plotly Dash
https://dash-bootstrap-components.opensource.faculty.ai/
Apache License 2.0
1.12k stars 220 forks source link

Container width is wrong #382

Closed dr-glenn closed 4 years ago

dr-glenn commented 4 years ago

Please refer to #286. The size of dbc.Container with the 4 colored rectangles measures as 1110x937 (my screen is 1920x1080 and of course some real estate is consumed by the browser). I expected the colored cells to occupy the entire view. Changing the browser window only alters the very wide margins on right and left. Using Chrome browser 81.0.4044.138 or Edge browser.

What is happening?

See above. I have tried various Container settings to affect width, to no avail.

What should be happening?

The colored cells should fill the browser view, shouldn't they? If I change dbc.Container to html.Div, then the colored cells do fill the window. But one problem remains: the window is not completely displayed - it can be scrolled vertically and horizontally by a small amount. The small amount appears to be approximately the width (or height) of a scrollbar.

Code

See program in #286. I have not changed it, except to try using html.Div in place of dbc.Container.

tcbegley commented 4 years ago

Hi @dr-glenn

The default Bootstrap container is fixed width by design. If you would like it to fill the horizontal space you can use a fluid container by setting fluid=True

app.layout = dbc.Container(..., fluid=True)

Let me know if that resolves your issue.

dr-glenn commented 4 years ago

It fixes the color box example, and also resolves the height issues I was seeing and no more odd layout about scrollbars (which I also mentioned). I don't understand why a Bootstrap container is fixed width by design. Unfortunately I now have a height problem with my real application. dcc.Graph components do not obey height as applied to dbc.Row, that is, className="h-25" or "h-75" is ignored and the graphs have height of 450px - the same problem that plagues all Dash users! Here is my application with fluid=False (I didn't include blank margins in the screenshot). dash-dbc-not-fluid And here it is with fluid=True. This is what I want to see ... except for the height problem. dash-dbc-fluid

tcbegley commented 4 years ago

You'll probably want to either set the height of the graph or set something like max-height on the row, looks like they're expanding to accommodate their content (your graphs).

dr-glenn commented 4 years ago

Please give me usable advice ... do I need to set max-height in pixels? My overall problem here is that I don't want to use pixels, I want relative units, like all good layouts should use. If I can't use relative units, such as className="h-75' or style height="75%", then either dash-bootstrap-components has a problem or more likely the fault lies with dash-core-components, which appear to not obey container style constraints. Please tell me which is the case so that we can close this issue.

tcbegley commented 4 years ago

Please give me usable advice ...

If you would like me to be more specific can you please share some example code? It's hard to know exactly what the issue is just from a screenshot.

Using your browsers developer tools you can check the height of the row / cols to figure out if they are the wrong height or if the graph is overflowing, that might at least help narrow down the issue.

dr-glenn commented 4 years ago

Sorry for the long delay responding - this is a volunteer project for me and I got wrapped up in other work. So here is a few hundred lines of code, including 65 lines of sample data - I hope that's OK. The title says data is every 5 minutes, but the sample data is one every 1h20m. I used browser developer tools and the rows are forced to a larger height to accommodate the dcc.Graph components which default to 450px and do not squeeze into the dbc style specifications. (I must apologize for failure to insert code here such that spaces are preserved. It doesn't matter if I use no formatting, "insert quote" or "insert code" - it just drops all leading spaces!)

# -*- coding: utf-8 -*-
import os
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import pandas as pd
import datetime as dt
from io import StringIO

# Dash callbacks: https://dash.plot.ly/getting-started-part-2
# wind direction polar plot with dcc: https://github.com/plotly/dash-sample-apps/blob/master/apps/dash-wind-streaming/app.py
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"}
datafile = 'ex.dat'
dat_names = ['wdate','wind_dir','wind_speed','wind_gust','col5','humidity','col7','temp_out','col9','col10']
TESTDATA = StringIO("""
202003190115 359.000 0.000 0.000 100.000 97.000 55.500 27.700 24.986 9.120
202003190235 359.000 0.000 0.000 100.000 97.000 55.100 28.100 24.986 9.120
202003190350 359.000 0.000 0.000 100.000 97.000 54.800 28.500 24.986 9.120
202003190515 359.000 5.000 8.000 100.000 97.000 54.500 28.400 24.986 9.120
202003190635 359.000 0.000 0.000 100.000 97.000 54.000 28.000 24.986 9.120
202003190750 359.000 0.000 0.000 100.000 97.000 53.100 27.700 24.986 9.120
202003190915 359.000 0.000 0.000 100.000 97.000 53.300 29.000 24.986 9.120
202003191035 359.000 0.000 0.000 100.000 98.000 54.600 31.300 24.986 9.120
202003191150 241.000 0.000 0.000 100.000 98.000 56.100 32.600 24.986 9.120
202003191315 359.000 0.000 0.000 100.000 97.000 58.700 34.600 24.986 9.120
202003191435 359.000 0.000 0.000 100.000 97.000 59.700 33.500 24.986 9.120
202003191550 290.000 0.000 0.000 100.000 98.000 59.700 33.200 24.986 9.130
202003191715 15.000 0.000 0.000 100.000 98.000 59.600 32.600 24.986 9.160
202003191835 23.000 0.000 0.000 100.000 98.000 59.600 33.200 24.986 9.160
202003191950 23.000 0.000 0.000 100.000 98.000 59.100 32.800 24.986 9.160
202003192115 18.000 0.000 0.000 100.000 98.000 58.400 32.100 24.986 9.160
202003192235 22.000 0.000 0.000 100.000 98.000 57.700 32.500 24.986 9.160
202003192350 37.000 0.000 0.000 100.000 98.000 57.100 32.100 24.986 9.170
202003200115 41.000 0.000 0.000 100.000 98.000 56.700 31.600 24.986 9.170
202003200235 89.000 0.000 0.000 100.000 98.000 56.300 31.200 24.986 9.170
202003200350 93.000 0.000 0.000 100.000 98.000 55.700 30.100 24.986 9.170
202003200515 90.000 0.000 0.000 100.000 98.000 55.400 30.600 24.986 9.170
202003200635 359.000 0.000 0.000 100.000 98.000 54.800 29.400 24.986 9.170
202003200750 359.000 0.000 0.000 100.000 98.000 54.200 30.500 24.986 9.170
202003200915 60.000 0.000 0.000 100.000 98.000 55.200 31.600 24.986 9.170
202003201035 97.000 0.000 0.000 100.000 98.000 56.500 32.800 24.986 9.170
202003201150 359.000 3.000 6.000 100.000 98.000 58.800 34.600 24.986 9.170
202003201315 359.000 0.000 0.000 100.000 97.000 60.300 35.900 24.986 9.170
202003201435 359.000 0.000 0.000 100.000 97.000 61.300 36.900 24.986 9.170
202003201550 10.000 2.000 3.000 100.000 97.000 62.100 37.700 24.986 9.170
202003201715 16.000 0.000 0.000 100.000 97.000 62.400 37.100 24.986 9.170
202003201835 32.000 0.000 0.000 100.000 98.000 62.200 35.900 24.986 9.170
202003201950 97.000 1.000 4.000 100.000 98.000 61.400 33.300 24.986 9.190
202003202115 359.000 0.000 0.000 100.000 98.000 60.500 34.800 24.986 9.190
202003202235 105.000 0.000 5.000 100.000 98.000 59.700 34.800 24.986 9.190
202003202350 105.000 0.000 0.000 100.000 98.000 59.300 34.400 24.986 9.200
202003210115 102.000 0.000 0.000 100.000 98.000 58.800 33.400 24.986 9.220
202003210235 359.000 0.000 0.000 100.000 98.000 58.200 32.500 24.986 9.250
202003210350 359.000 0.000 6.000 100.000 98.000 57.700 32.800 24.986 9.260
202003210515 359.000 0.000 0.000 100.000 98.000 57.300 33.200 24.986 9.270
202003210635 359.000 0.000 0.000 100.000 98.000 57.000 32.800 24.986 9.280
202003210750 158.000 0.000 0.000 100.000 98.000 56.700 32.600 24.986 9.290
202003210915 158.000 0.000 0.000 100.000 98.000 56.700 32.000 24.986 9.320
202003211035 157.000 0.000 2.000 100.000 98.000 56.800 32.300 24.986 9.380
202003211150 162.000 0.000 6.000 100.000 98.000 57.100 34.200 24.986 9.410
202003211315 115.000 11.000 17.000 100.000 98.000 56.800 35.600 24.986 9.410
202003211435 137.000 2.000 6.000 100.000 98.000 57.400 36.000 24.986 9.410
202003211550 359.000 0.000 9.000 100.000 98.000 59.400 38.700 24.986 9.410
202003211715 359.000 0.000 0.000 100.000 98.000 61.100 38.000 24.986 9.410
202003211835 359.000 0.000 0.000 100.000 98.000 61.300 35.900 24.986 9.410
202003211950 359.000 0.000 4.000 100.000 98.000 60.700 36.200 24.986 9.410
202003212115 116.000 2.000 5.000 100.000 98.000 60.000 35.600 24.986 9.410
202003212235 359.000 7.000 10.000 100.000 98.000 59.400 36.000 24.986 9.410
202003212350 91.000 0.000 5.000 100.000 98.000 58.500 35.800 24.986 9.410
202003220115 359.000 10.000 11.000 100.000 98.000 58.100 35.900 24.986 9.410
202003220235 58.000 4.000 12.000 100.000 98.000 57.600 35.900 24.986 9.410
202003220350 67.000 1.000 7.000 100.000 98.000 57.300 36.700 24.986 9.410
202003220515 51.000 0.000 7.000 100.000 98.000 57.100 36.900 24.986 9.410
202003220635 66.000 0.000 8.000 100.000 98.000 56.800 35.900 24.986 9.410
202003220750 56.000 0.000 7.000 100.000 98.000 56.700 35.700 24.986 9.410
202003220915 66.000 0.000 9.000 100.000 99.000 56.400 35.800 24.986 9.420
202003221035 78.000 0.000 2.000 100.000 98.000 56.700 36.700 24.986 9.420
202003221150 359.000 0.000 7.000 100.000 98.000 56.800 37.400 24.986 9.440
202003221315 49.000 5.000 9.000 100.000 99.000 57.300 38.800 24.986 9.450
202003221435 359.000 0.000 3.000 100.000 98.000 58.100 39.700 24.986 9.450
202003221550 359.000 0.000 0.000 100.000 99.000 58.500 39.400 24.986 9.450
202003221705 359.000 0.000 0.000 100.000 99.000 59.000 39.800 24.986 9.460
""")
df = pd.read_csv(TESTDATA, sep=' ',names=dat_names,index_col=0,parse_dates=True)
df = df.drop_duplicates()
df.index = pd.to_datetime(df.index,format="%Y%m%d%H%M")
# the full index range
date_limits = [df.index.min(),df.index.max()]

N_DAYS = 20     # start with range slider of last N days
# use datetime, set earliest to 00:00 and latest to 23:59
datetime1 = dt.datetime(date_limits[1].year, date_limits[1].month, date_limits[1].day,hour=23,minute=59)
date1 = dt.date(date_limits[1].year, date_limits[1].month, date_limits[1].day)
datetime0 = dt.datetime(date_limits[0].year, date_limits[0].month, date_limits[0].day)
date0 = dt.date(date_limits[0].year, date_limits[0].month, date_limits[0].day)
date_range = date1 - date0  # integer number of days
# date0 no more than N_DAYS before date1
date0 = date0 if date_range <= dt.timedelta(days=N_DAYS) else date1 - dt.timedelta(days=N_DAYS)

df30 = df.loc[datetime0 : datetime1]

# TODO: I can select specific date like this: df.loc['2020-02-06'], even if the index contains HHMMSS.
# TODO: How to select a range of dates?

# generate a list of dates for the slider
def gen_date_list(date0,date1):
    d0 = dt.date(date0.year,date0.month,date0.day)
    d1 = dt.date(date1.year,date1.month,date1.day)
    ndate = (d1-d0).days + 1
    day_delta = dt.timedelta(days=1)
    dlist = [d0 + i*day_delta for i in range(ndate)]
    return dlist

day_list = gen_date_list(date0,date1)

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP,])

# Next dbc.Row contains two graphs side-by-side
row1 = dbc.Row(
    # style for Div that contains both wind speed and wind direction graphs
    # margin-bottom required for labels on date selector slider
    children=[
        # XY graph of wind speed, time on x-axis
        dbc.Col(width = 7, style={'height':'100%'}, children=[
            dcc.Graph(
                id='wind-date-plot',
                style={'height': 'inherit'}
            ),
            html.Div(
                # date slider sits underneath the wind-speed plot
                dcc.Slider(
                    id='wind-date-slider',
                    included=False,
                    min=0,
                    max=len(day_list) - 1,
                    value=len(day_list) - 1,  # start at last day
                    marks={i: {'label': str(dm), 'style': {'transform': 'rotate(-40deg) translate(-40px,-20px)'}} for
                           i, dm in enumerate(day_list) if i % 2 == 0},
                    dots=True,
                ),
                # style for Div that contains Slider
                style={'width': "70%", "margin-left": "100px", }
            )
        ],
        ),
        # polar plot of wind speed and direction with hour slider
        dbc.Col(
            # style for Span that contains Graph and Slider
            #style=dict(display="inline-block", width="30%"),
            width = 3, style={'height':'100%'},
            children=[
                dcc.Graph(
                    id='wind-dir-plot',
                    style={'height': 'inherit'}
                ),
                html.Div(
                    # style for Div that contains Slider
                    style={'margin-top': '20px'},
                    children=[
                        dcc.Slider(
                            # style={'height':'inherit'},
                            id='hour-slider',
                            included=False,
                            min=0,
                            max=23,
                            value=0,  # start at zero hour
                            marks={str(hour): str(hour) for hour in range(0, 23, 2)},
                            step=None
                        ),
                    ],
                )],
        ),
    ], className='mh-75'
)

# Second row of graphs
row2 = dbc.Row(children=[
        dbc.Col(width=5, children=[dcc.Graph(id='temp-out',style={'height':'inherit'}),]),
        dbc.Col(width=5, children=[dcc.Graph(id='rel-hum',style={'height':'inherit'}),]),
        ], className='mh-25')
# must set fluid=True to occupy full width
app.layout = dbc.Container(fluid=True,
    children=[row1, row2],
    )

@app.callback(
    Output('wind-date-plot', 'figure'),
    [Input('wind-date-slider', 'value')])
def update_wind_speed_figure(selected_day):
    f_df = df30[str(date0+dt.timedelta(days=selected_day))]
    traces = []
    traces.append(dict(x=f_df.index,y=f_df['wind_speed'],type='line',name=u'wind_speed'))
    traces.append(dict(x=f_df.index,y=f_df['wind_gust'],type='line',name=u'wind_gust'))
    return {
        'data': traces,
        'layout': dict(title={'text':'Wind Speed (every 5 minutes)','y':'0.8'}),
    }

@app.callback(
    Output('wind-dir-plot', 'figure'),
    [Input('wind-date-slider', 'value'),
     Input('hour-slider','value')])
def update_wind_dir_figure(selected_day, selected_hour):
    f_df = df30[str(date0+dt.timedelta(days=selected_day))]
    # Max wind speed in this time
    wmax = f_df['wind_speed'].max()
    wmax = int(round(wmax,-1))
    tick_int = 10 if wmax >= 20 else 5
    # Get 4 hour slice for this graph
    d0 = f_df.index[0]   # it's a datetime
    hour0 = dt.timedelta(hours=selected_hour)
    h1 = selected_hour+4 if selected_hour <= 20 else 24
    hour1 = dt.timedelta(hours=h1)
    f_df = f_df.loc[str(d0+hour0) : str(d0+hour1)]
    # Wind direction of 359 is almost certainly bogus, so remove
    winds = f_df.loc[df['wind_dir'] != 359.0][['wind_dir','wind_speed']]
    val = winds["wind_speed"]
    direction = winds["wind_dir"]

    data = [
        dict(
            type="scatterpolar",
            r=val,
            theta=direction,
            mode="markers",
            marker=dict(color='yellow'),
        )
    ]
    layout = dict(
        paper_bgcolor=app_color["graph_bg"],
        font={"color": "#fff"},
        autosize=False,
        polar=dict(
            bgcolor=app_color["graph_line"],
            radialaxis=dict(range=[0, wmax], angle=0, dtick=tick_int),
            angularaxis=dict(showline=False, tickcolor="white",rotation=90,direction='clockwise'),
        ),
        showlegend=False,
        title='Wind Direction (4 hours)',
    )
    return {
        'data': data,
        'layout': layout,
    }

@app.callback(
    Output('temp-out', 'figure'),
    [Input('wind-date-slider', 'value')])
def update_temp_out_figure(selected_day):
    f_df = df30[str(date0+dt.timedelta(days=selected_day))]
    traces = []
    traces.append(dict(x=f_df.index,y=f_df['temp_out'],type='line',name=u'temperature'))
    return {
        'data': traces,
        'layout': dict(title={'text':'Temperature Outside','y':'0.8'}),
    }

@app.callback(
    Output('rel-hum', 'figure'),
    [Input('wind-date-slider', 'value')])
def update_rel_hum_figure(selected_day):
    f_df = df30[str(date0+dt.timedelta(days=selected_day))]
    traces = []
    traces.append(dict(x=f_df.index,y=f_df['humidity'],type='line',name=u'humidity'))
    return {
        'data': traces,
        'layout': dict(title={'text':'Relative Humidity','y':'0.8'},),
    }

if __name__ == '__main__':
    server = app.server
    app.run_server(debug=True, port=8050)
else:   # not __main__
    # when running by wsgi script
    app.config.update({
        'url_base_pathname':'/OOS-data/',
        'routes_pathname_prefix':'/OOS-data/',
        'requests_pathname_prefix':'/OOS-data/',
    })
    server = app.server
tcbegley commented 4 years ago

Hey @dr-glenn

~If you're having problems formatting the code here, any chance you could paste it into https://gist.github.com/ or similar instead? Will make it much quicker for me to try and run the app locally and help resolve your issue.~

All good, turns out I have the power to edit your messages, which meant I could copy your code as plain text rather than formatted Markdown.

tcbegley commented 4 years ago

Hey @dr-glenn

I took a look at your app. Seems that you setting height=100% using the style argument of one of your plots was causing a whole load of trouble. Once I deleted that everything looked much better. Also the column widths you were specifying always added up to 10, whereas Bootstrap is based on a 12 column layout. I changed 7, 3 and 5, 5 to 8, 4 and 6, 6 respectively.

Here's a screenshot with some borders added so you can see everything is aligned nicely

image

And here is the full source code that I was working with.

dr-glenn commented 4 years ago

Thank you, I created a gist as suggested: https://gist.github.com/dr-glenn/cfa3ffb4ef8c4d46dc0901be259706d9.js. Changed the column counts as suggested so they add up to 12. Eliminated style={'height':'100%'} in two places. The two dbc.Row are not obeying classname='wh-75' and 'wh-25'. All four graphs have the Dash default height of 450px. So then I deleted style={'height':'inherit'}, recommended by chriddyp in https://community.plotly.com/t/cant-seem-to-change-default-height-on-graph/6742/27 to solve dcc.Graph height style problems. Nothing is changed, the graphs are all still 450px high, so the dbc 'wh-75' and 'wh-25' styles are ignored.

tcbegley commented 4 years ago

Hey @dr-glenn, the link to the Gist didn't work for me for some reason.

Anyway, I had another look and stripped out a lot of the style that was in there to build it up again from scratch. In the end I think I got the layout you were looking for doing the following

Here's the gist.

And a screenshot

image

dr-glenn commented 4 years ago

@tcbegley Thank you so much! I'm now seeing the same as you. What a mess it is to have to use className specifications both on the dcc.Graph and the dbc.Col container. Now I have two questions:

  1. The bottom row of graphs don't display the expected vertical height of 'h-25'. Browser developer tools shows me that the entire dbc.Col object is 25% height, but the dcc.Graph has a huge amount of white space on top. In fact, just measuring the graph itself (not including axis labels), the height seems to be about 25% of the Col object! The top row graphs - it's more difficult to estimate - may be in the neighborhood of 75%.
  2. Where is the documentation for these CSS classes? Somewhere I read that I should use 'mh-75', not 'h-75'. What other heights are there? Now I see that w3schools has some docs: https://www.w3schools.com/bootstrap4/bootstrap_utilities.asp. But I had to google 'bootstrap h-75' to find appropriate documentation. Just searching for 'bootstrap css' is a real quagmire.
tcbegley commented 4 years ago

Great, glad it's working now.

  1. Yes, the margins of Plotly figures don't always scale so well it seems. I think you'll probably have to play around with setting them yourself. See the docs here
  2. I normally just go straight to the Bootstrap docs for this sort of thing. Alternatively I might look directly at the compiled Bootstrap CSS on their GitHub repo and search for the class I think should exist.
dr-glenn commented 4 years ago

I now think that the height is a plotly problem. Tell me if you agree. On the two bottom graphs the dbc.Col is supposed to be 'h-25'. In my browser Developer Tools shows that "div class='h-25' has a height of 234.25 px. This is correct! The vertical padding, border and margins are 0 px. Inside are nested objects: "div class=js-plotly-plot", "div class="plot-container" and "div class=svg-container". All of these divs have a specified height of 234.25. 234 px measures 6.3 cm on my screen; the graph itself (not including x-axis annotation) measures 1.3 cm; with annotation it might be 1.7 cm. Conclusion: the plotly code has chosen to squeeze the height. I don't imagine that dbc code would do that - am I correct?

As for your suggestion that I should look into plotly figures, the documentation is so horrible that I just don't know what I could do. Anyway, each nested container specifies the full height of 234.25 px and there are 0 border, padding, margins, so what options are available?

tcbegley commented 4 years ago

Yes, if the column is the right height, then there's little else to be done on the Bootstrap side.

If the Plotly docs are confusing, then you could try posting on the Plotly forum? The Plotly devs themselves often chip in. I thought there was a way to reduce the surrounding whitespace, but I am not an expert unfortunately. Someone over there though might just know the right way to do this.

dr-glenn commented 4 years ago

Apparently dash-bootstrap-components is doing what it promises, but dash-core-components behaves poorly when dcc.Graph is constrained as when I put dcc.Graph inside a dbc cell with "div class='h-25' ". So I'll return to Dash/Plotly forums and see if I can get this resolved, Thank you all for your help here - so much better than Dash community.