Примеры компонентов Bootstrap для отзывчивых панелей мониторинга

Почему адаптивный дизайн

Мобильный просмотр огромен, на него приходится примерно половина мирового веб-трафика, и он, вероятно, останется одним из самых быстрорастущих способов использования веб-медиа. Поскольку мобильный просмотр широко распространен, важно понимать, понадобится ли пользователям ваших информационных панелей адаптивный дизайн. Например, на работе я создаю информационные панели для деловых людей, которые путешествуют и взаимодействуют с клиентами. Мне нужно убедиться, что визуализация и навигация не сломаются, если моим пользователям потребуется отображать данные со своего телефона или планшета.

Хотя я знаю достаточно CSS, чтобы стилизовать простой веб-сайт, я считаю создание таблиц стилей немного утомительным. Dash упрощает применение ваших собственных таблиц стилей, если вы умеете их создавать; однако, если вы не знакомы с CSS или не хотите тратить время на создание своего собственного, Dash может использовать Bootstrap CSS, который упрощает объединение стиля и макета страницы. Bootstra - одна из самых популярных в мире интерфейсных сред для создания адаптивных, удобных для мобильных устройств сайтов.

Знакомство с CSS и Dash

В этой статье я рассмотрю основы макета CSS-сетки Bootstrap и рассмотрю некоторые интересные dash-bootstrap-components и обратные вызовы, такие как Accordion , Modal и Jumbotron .

Dash - это фреймворк для Python, написанный поверх Flask, Plotly.js и React.js. Приложения Dash состоят из макетов и обратных вызовов:

Макет

Макет состоит из дерева компонентов, которые описывают, как выглядит приложение и как пользователи воспринимают контент.

Обратные вызовы

Обратные вызовы делают приложения Dash интерактивными. Обратные вызовы - это функции Python, которые автоматически вызываются при изменении свойства input.

Если вы не знакомы с Dash или нуждаетесь в быстром обновлении, ознакомьтесь с моими вводными статьями. Весь код доступен в конце статьи!





Что такое CSS?

Каскадные таблицы стилей, или CSS, обычно представляют собой внешние файлы, которые могут описывать, как элементы HTML на вашем веб-сайте отображаются на устройствах. Таблицы стилей полезны, потому что они могут влиять на несколько страниц одновременно, тем самым экономя много времени и усилий, если на веб-сайте несколько страниц. CSS позволяет мне по существу определять стиль моих информационных панелей, и это влияет на макет на экранах разных размеров.

В традиционном файле CSS вы увидите подобный синтаксис, описывающий, как должны выглядеть элементы HTML:

Обратите внимание, как таблица стилей описывает стиль и цвета для H1, H2 и P HTML теги. Таблицы стилей можно импортировать в Dash.

Загрузочный CSS в Dash

Подобно тому, как библиотека dash-html-components позволяет применять HTML с помощью Python, библиотека dash-bootstrap-components позволяет применять Начальные компоненты начальной загрузки, на которые влияет CSS-фреймворк Bootstrap. Процитируем документацию:

dash-bootstrap-components полагается на Twitter Bootstrap. Чтобы использовать этот пакет, вставьте таблицу стилей Bootstrap в свое приложение. Для удобства ссылки на Bootstrap CSS, размещенные на bootstrapcdn, включены как часть модуля themes

Чтобы настроить приложение для запуска с использованием Bootstrap CSS, вам необходимо включить этот фрагмент стандартного кода. Обратите внимание, что здесь устанавливается внешняя таблица стилей. Поскольку я использую Bootstrap CSS, я установил его с помощью [dbc.themes.BOOTSTRAP].

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

Установка и зависимости

Легко установите dash-bootstrap-components с помощью pip или Anaconda:

pip install dash-bootstrap-components
OR
conda install -c conda-forge dash-bootstrap-components

При импорте зависимостей включите библиотеку dash_bootstrap_components и присвойте ей псевдоним dbc.

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html

Изучение компонентов макета Bootstrap

На мой взгляд, Компоненты компоновки - это самое большое преимущество использования библиотеки, но после обзора сетки я покажу несколько других интересных компонентов.

Адаптивная сетка и удобные оболочки контейнера допускают множество настроек. Сетка Bootstrap использует серию контейнеров, строк и столбцов для компоновки и выравнивания содержимого. Они были включены в качестве компонентов в библиотеку dash-bootstrap-components как Container, Row и Col.

Компонент Row - это оболочка для столбцов. Макет вашего приложения должен быть построен как ряд строк столбцов.

При использовании макета сетки содержимое следует размещать в столбцах, и только компоненты Col должны быть непосредственными дочерними элементами Row.

Сетка Bootstrap содержит 12 столбцов и пять уровней отзывчивости. Визуализировать столбцы легко с помощью компонента dbc.Alert ().

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
body = html.Div([
    html.H1("Bootstrap Grid System Example")
    , dbc.Row(dbc.Col(html.Div(dbc.Alert("This is one column", color="primary"))))
    , dbc.Row([
            dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")))
            , dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")))
            , dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")))
            ])
        ])
app.layout = html.Div([body])

if __name__ == "__main__":
    app.run_server(debug = True)

Обратите внимание, что тело построено с использованием серии строк и столбцов, как это предлагается в документации. Обратите внимание: dbc.Col является непосредственным дочерним элементом dbc.Row, но за dbc.Col следует знакомый Dash HTML компоненты.

По умолчанию столбец расширяется и занимает весь экран. Чтобы лучше контролировать расширение столбцов, попробуйте обернуть сетку в dbc.Container или использовать аргумент width, чтобы присвоить ширину колонка.

Обратите внимание, что между столбцами есть зазор. Эти пробелы называются Промежуточными полями и являются частью компонента dbc.Row. Чтобы удалить желоба, установите no_gutters = True.

Ниже приведен более подробный пример того, как можно стилизовать сетку. Поскольку строки составляют 12 столбцов, проще всего использовать числа с коэффициентом 12 при работе с несколькими столбцами в строке. Как видно из примера, я использую несколько комбинаций размеров столбцов.

body = html.Div([html.H1("Bootstrap Grid System Example")
, html.H4("no_gutters = False")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3)
])
, html.H4("no_gutters = True")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3)
], no_gutters = True)    
, html.H3("Examples of justify property")
, html.H4("start, center, end, between, around")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=4),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=4),
],
justify="start")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
],
justify="center")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of three columns")), width=3)
    , dbc.Col(html.Div(dbc.Alert("One of three columns")), width=3)
    , dbc.Col(html.Div(dbc.Alert("One of three columns")), width=3)
],
justify="end")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3)
    , dbc.Col(html.Div(dbc.Alert("One of three columns")), width=3)
],
justify="between")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=4),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=4),
],
justify="around")
, html.H4("Container Example")
, dbc.Container([
    dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3)
])
, html.H4("no_gutters = True")
, dbc.Row([
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3),
    dbc.Col(html.Div(dbc.Alert("One of two columns")), width=3)
], no_gutters = True)    
])
])

Обратите внимание, что желоба можно легко удалить, а строки можно выровнять.
Обратите внимание, как пример контейнера по умолчанию центрирует и сжимает столбцы.

Помимо использования аргумента width, настройте размер, порядок и смещение ваших столбцов на различных устройствах с помощью xs, sm, md, lg и xl аргументы ключевого слова.

body = html.Div([
html.H1("Bootstrap Grid System Example")
, dbc.Row(dbc.Col(html.Div(dbc.Alert("This is one column", color="primary"))))
, dbc.Row([
        dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")), lg=3, md=4, xs=12)
        , dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")), lg=3, md=4, xs=12)
        , dbc.Col(html.Div(dbc.Alert("One of three columns", color="primary")), lg=3, md=4, xs=12)
        ])
    ])

Обратите внимание, что размер столбца меняется по мере уменьшения размеров экранов. Например, использование xs = 12 приводит к тому, что столбец занимает всю строку, когда экран слишком мал, поскольку система сетки состоит из 12 столбцов.

Вкратце, в этом заключается простота и мощность адаптивной сеточной системы Bootstrap. С помощью нескольких строк кода можно создать адаптивный макет, который будет работать на экранах разных размеров. Сетка легко применяется к существующим панелям мониторинга.

Улучшение приборной панели

В предыдущей статье я объяснил, как получить данные из Reddit и отобразить их с помощью библиотеки компонентов Таблица данных в Dash. Чтобы дать более реалистичные примеры применения компонентов Bootstrap, я применю их к панели управления Reddit. Если вы уже знакомы с Reddit, я включаю в эту статью весь приведенный ниже стартовый код; в противном случае обратитесь к предыдущей статье для получения инструкций по созданию ключей Reddit API!



Я объясню, как преобразовать исходную панель Reddit Dashboard в изображение ниже с помощью Bootstrap CSS. Я добавлю Jumbotron, C ollapse Accordion и Modal, который генерирует всплывающее окно, когда Создать графики нажата кнопка:

Стартовый код

Это начальный код, который предоставит вам основную панель управления Reddit. Предполагается, что вы используете файл конфигурации для передачи ключей API в код. По умолчанию используется subreddit wallstreetbets, поэтому обновите это значение, если хотите, чтобы оно по умолчанию было другим.

import dash
import dash_html_components as html
import dash_core_components as dcc
import dash_table
import pandas as pd
import praw
import pandas as pd
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from config import cid, csec, uag
reddit = praw.Reddit(client_id= cid, client_secret= csec, user_agent= uag)
posts = []
new_bets = reddit.subreddit('wallstreetbets').new(limit=100)
for post in new_bets:
    posts.append([post.title, post.score, post.num_comments, post.selftext, post.created, post.pinned, post.total_awards_received])
posts = pd.DataFrame(posts,columns=['title', 'score', 'comments', 'post', 'created', 'pinned', 'total awards'])
def get_features(dataframe):
    df = posts.copy()
df['words'] = df['post'].apply(lambda x : len(x.split()))
    df['chars'] = df['post'].apply(lambda x : len(x.replace(" ","")))
    df['word density'] = (df['words'] / (df['chars'] + 1)).round(3)
    df['unique words'] = df['post'].apply(lambda x: len(set(w for w in x.split())))
    df['unique density'] = (df['unique words'] / df['words']).round(3)
return df
df = get_features(posts)
app.layout = html.Div([
    html.P(html.Button('Refresh', id='refresh'))
    ,html.P(html.Div(html.H3('Enter Subreddit')))
    ,dcc.Input(id='input-1-state', type='text', value='wallstreetbets')
    ,dash_table.DataTable(
    id='table'
    ,columns=[{"name": i, "id": i} for i in df.columns]
    ,fixed_rows={ 'headers': True, 'data': 0 }
    ,data=df.to_dict('records')
)
])
@app.callback(Output('table', 'data'),
              [Input('refresh', 'n_clicks')],
              [State('input-1-state', 'value')
               ])
def update_data(n_clicks, subreddits):
    dff = df
    if subreddits is None:
        subreddits = 'wallstreetbets'
    else:
        subreddits
    if n_clicks is None:
        raise PreventUpdate
    else:
        posts = []
        new_bets = reddit.subreddit(subreddits).hot(limit=100)
        for post in new_bets:
            posts.append([post.title, post.score, post.num_comments, post.selftext, post.created, post.pinned, post.total_awards_received])
        posts = pd.DataFrame(posts,columns=['title', 'score', 'comments', 'post', 'created', 'pinned', 'total awards'])
        dff = get_features(posts)
    
    return dff.to_dict('records')

Добавление Jumbotron

Jumbotron - легкий компонент, который помогает контенту выделяться. Он занимает много места на экране, поэтому отлично подходит для отображения кнопок и сообщений. Джамботрон - это большая серая рамка, окружающая поле ввода и кнопку.

Вот часть кода, изменяемая для компонента Bootstrap:

# Original
html.Div([
    html.P(html.Button('Refresh', id='refresh'))
    ,html.P(html.Div(html.H3('Enter Subreddit')))
    ,dcc.Input(id='input-1-state', type='text', value='wallstreetbets')
])

Я буду использовать dbc.Jumbotron, чтобы обернуть html.Div, который содержит компоненты ввода и кнопки. Жирным шрифтом выделен исходный код.

dbc.Jumbotron([
dbc.Row(dbc.Col([
html.P(html.Div(html.H3('Enter Subreddit')))
, dcc.Input(id='input-1-state', type='text', value='wallstreetbets')
, html.P(html.Button('Generate Graphs', id='refresh'))
   ], width = 6)
  , justify = 'center')# end row
  ], fluid = True)

Обратите внимание, что dbc.Jumbotron оборачивает dbc.Row и dbc.Col, поэтому я могу воспользоваться преимуществами ширины и обосновать аргументы.
Обратите внимание, что я выравниваю содержимое по центру, чтобы его было легче заметить.
Обратите внимание, что я установил fluid = True аргумент в пользу Jumbotron. Жамботрон будет иметь полную ширину и квадратные углы.

Добавление модального окна

Модальные окна - еще один полезный инструмент для привлечения внимания к сообщениям или функциям. Используйте компонент dbc.Modal для добавления пользовательских уведомлений или настраиваемого содержимого, которое появляется после того, как пользователь выполняет действие. Например, когда пользователь нажимает кнопку Создать графики, Modal создает всплывающее окно, которое позволяет пользователю узнать, что графики были созданы.

Модальное окно помещается в Jumbotron. Я его туда положил, так как он взаимодействует с кнопкой.

dbc.Jumbotron([
dbc.Row(dbc.Col([
html.P(html.Div(html.H3('Enter Subreddit')))
, dcc.Input(id='input-1-state', type='text', value='wallstreetbets')
, html.P(html.Button('Generate Graphs', id='refresh'))
, dbc.Modal(
     [
   dbc.ModalHeader("Generating"),
   dbc.ModalBody("The graphs have generated"),
   dbc.ModalFooter(
      dbc.Button("Close", id="close", className="ml-auto")
      ),
      ],
      id="modal",
    )
   ], width = 6)
  , justify = 'center')# end row
  ], fluid = True)

Обратите внимание, что модальный создается с использованием компонентов Header, Body и Footer. По умолчанию модальное окно можно закрыть, щелкнув за пределами модального окна или нажав клавишу escape.

Размер модального окна можно настроить с помощью свойства размер. Возможные варианты: sm, lg или xl для маленького, большого или очень большого модального окна.

Для работы модальное окно использует обратный вызов. Используя обратные вызовы, можно установить для свойства is_open значение Истина или Ложь. , в зависимости от того, как я хочу его отображать.

@app.callback(
    Output("modal", "is_open"),
    [Input("open", "n_clicks"), Input("close", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open

Добавление аккордеона с свертыванием

Используя компонент dbc.Collapse, можно переключать видимость контента. Комбинируя компоненты Свернуть и Карточка, вы можете расширить стандартное поведение свертывания, чтобы создать гармошку.

Создать аккордеон сложно, потому что Dash выдаст ошибку, если компоненты имеют одинаковый идентификатор. Таким образом, с помощью функции я могу программно создавать уникальные идентификаторы для компонентов Свернуть и Карточка в аккордеоне. Это также затрудняет заполнение карточек в аккордеоне, но я использую список в функции, чтобы упростить логику пополнения!

def make_item(i):
    # we use this function to make the example items to avoid code duplication
    graph_info = [0,'Displays words on x axis vs score on y axis.'
                    , 'Displays words on x axis vs word density on y axis.'
                    , 'Displays words on x axis vs unique density on y axis.']
    return dbc.Card(
        [
            dbc.CardHeader(
                html.H2(
                    dbc.Button(
                        f"Graph {i} Info",
                        color="link",
                        id=f"group-{i}-toggle",
                    )
                )
            ),
            dbc.Collapse(
                dbc.CardBody(graph_info[i]),
                id=f"collapse-{i}",
            ),
        ]
    )

Функция make_item (i) принимает числовое значение и выводит компонент dbc.Card, который является оболочкой для Collapse используется для создания эффекта аккордеона.
Обратите внимание на список graph_info. Это список, который я использую для заполнения содержимого dbc.CardBody, поскольку я могу передать соответствующее числовое значение в индекс списка. Например, если я передаю 1 в make_item (1), я верну значение с индексом 1 в моем списке graph_info. Элемент в первом индексе следующий:

Displays words on x axis vs score on y axis.

Конструирование аккордеона

Чтобы построить аккордеон, я вызываю функции и заключаю их в компонент html.Div.

accordion = html.Div([make_item(1), make_item(2), make_item(3)], className="accordion")

Я добавляю его в основной список html.Div между jumbotron и таблицей данных.

Аккордеонный обратный звонок

Я взял шаблонный обратный вызов из документации. В зависимости от количества карточек в гармошке увеличьте диапазон для вывода, ввода и состояния.

@app.callback(
    [Output(f"collapse-{i}", "is_open") for i in range(1, 4)],
    [Input(f"group-{i}-toggle", "n_clicks") for i in range(1, 4)],
    [State(f"collapse-{i}", "is_open") for i in range(1, 4)],
)
def toggle_accordion(n1, n2, n3, is_open1, is_open2, is_open3):
    ctx = dash.callback_context
if not ctx.triggered:
        return ""
    else:
        button_id = ctx.triggered[0]["prop_id"].split(".")[0]
if button_id == "group-1-toggle" and n1:
        return not is_open1, False, False
    elif button_id == "group-2-toggle" and n2:
        return False, not is_open2, False
    elif button_id == "group-3-toggle" and n3:
        return False, False, not is_open3
    return False, False, False

Полная панель управления

После заполнения панели мониторинга легко увидеть, как Bootstrap CSS for Dash представляет собой мощную библиотеку компонентов, которая упрощает форматирование содержимого для просмотра на мобильных устройствах. Я прошелся по компонентам Адаптивная сетка, Jumbotron, Свернуть и Модальный, чтобы продемонстрировать некоторые из более продвинутых способов. Библиотека компонентов Bootstrap может повысить удобство использования приборной панели. Ниже приведен полный код панели управления. Ознакомьтесь с другими моими руководствами, если вы тоже новичок в программировании или науке о данных!





Благодарю вас!

- Эрик Клеппен

Код

Вот полный код. Помните, предполагается, что вы используете файл конфигурации для импорта ключей API.

import dash
import dash_html_components as html
import dash_core_components as dcc
import dash_table
import pandas as pd
import praw
import pandas as pd
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from config import cid, csec, uag
reddit = praw.Reddit(client_id= cid, client_secret= csec, user_agent= uag)
posts = []
new_bets = reddit.subreddit('wallstreetbets').hot(limit=100)
for post in new_bets:
    posts.append([post.title, post.score
    , post.num_comments, post.selftext
    , post.created, post.pinned, post.total_awards_received])
posts = pd.DataFrame(posts,columns=['title', 'score', 'comments', 'post', 'created', 'pinned', 'total awards'])
def get_features(dataframe):
    df = posts.copy()
df['words'] = df['post'].apply(lambda x : len(x.split()))
    df['chars'] = df['post'].apply(lambda x : len(x.replace(" ","")))
    df['word density'] = (df['words'] / (df['chars'] + 1)).round(3)
    df['unique words'] = df['post'].apply(lambda x: len(set(w for w in x.split())))
    df['unique density'] = (df['unique words'] / df['words']).round(3)
return df
df = get_features(posts)
def make_item(i):
    # we use this function to make the example items to avoid code duplication
    graph_info = [0,'Displays words on x axis vs score on y axis.'
                    , 'Displays words on x axis vs word density on y axis.'
                    , 'Displays words on x axis vs unique density on y axis.']
    return dbc.Card(
        [
            dbc.CardHeader(
                html.H2(
                    dbc.Button(
                        f"Graph {i} Info",
                        color="link",
                        id=f"group-{i}-toggle",
                    )
                )
            ),
            dbc.Collapse(
                dbc.CardBody(graph_info[i]),
                id=f"collapse-{i}",
            ),
        ]
    )
accordion = html.Div(
    [make_item(1), make_item(2), make_item(3)], className="accordion"
)
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout =  html.Div([
dbc.Jumbotron([
dbc.Row(dbc.Col([
        html.P(html.Div(html.H3('Enter Subreddit')))
        , dcc.Input(id='input-1-state', type='text', value='wallstreetbets')
        , html.P(html.Button('Generate Graphs', id='refresh'))
        , dbc.Modal(
                [
                    dbc.ModalHeader("Generating"),
                    dbc.ModalBody("The graphs have generated"),
                    dbc.ModalFooter(
                        dbc.Button("Close", id="close", className="ml-auto")
                    ),
                ],
                id="modal",
            )
        ], width = 6)
        , justify = 'center')# end row
        ], fluid = True)
    
    , html.Div(dbc.Row(dbc.Col(accordion ,width = 8),justify = 'center'))    
    
    , html.Div([    
        dbc.Row(dbc.Col(dash_table.DataTable(
        id='table'
        , columns=[{"name": i, "id": i} for i in df.columns]
        , data=df.to_dict('records')
        #, fixed_rows={ 'headers': True, 'data': 0 }
        , style_cell_conditional=[
            {'if': {'column_id': 'title'},
            'width': '150px'
            ,'padding': '15px'},
            {'if': {'column_id': 'post'},
            'width': '400px'
            }
            ,{'if': {'column_id': 'words'},
            'width': '65px'
            }
            ,{'if': {'column_id': 'chars'},
            'width': '65px'
            }
        ]
        ,style_cell={
            'overflowX': 'hidden',
            'textOverflow': 'ellipsis',
            'maxWidth': '25px',
        'textAlign': 'left'
        }
        , style_table={
            'maxHeight': '550px'
            #,'maxWidth': '800px'
            ,'overflowY': 'scroll'
            ,'overflowX': 'hidden'
        }
), width = 10), justify = 'center')# end dt
    ], style = {'background-color': 'lightblue'
                #,'margin-bottom': '5px' 
                , 'padding' : '50px'  
                    }) #end div
,  dbc.Container(
            html.Div(id = 'graph-container')
    )
])#end div
@app.callback(
    [Output(f"collapse-{i}", "is_open") for i in range(1, 4)],
    [Input(f"group-{i}-toggle", "n_clicks") for i in range(1, 4)],
    [State(f"collapse-{i}", "is_open") for i in range(1, 4)],
)
def toggle_accordion(n1, n2, n3, is_open1, is_open2, is_open3):
    ctx = dash.callback_context
if not ctx.triggered:
        return ""
    else:
        button_id = ctx.triggered[0]["prop_id"].split(".")[0]
if button_id == "group-1-toggle" and n1:
        return not is_open1, False, False
    elif button_id == "group-2-toggle" and n2:
        return False, not is_open2, False
    elif button_id == "group-3-toggle" and n3:
        return False, False, not is_open3
    return False, False, False
@app.callback(
    Output("modal", "is_open"),
    [Input("refresh", "n_clicks"), Input("close", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open
@app.callback(
    Output('graph-container', "children"),
    [Input('table', "data")])
def update_graph(rows):
    dff = pd.DataFrame(rows)
    return html.Div(
        [
            dcc.Graph(
                id=column,
                figure={
                    "data": [
                        {
                            "x": dff["words"],
                            "y": dff[column] if column in dff else [],
                            "type": "bar",
                            "marker": {"color": "#0074D9"},
                        }
                    ],
                    "layout": {
                        "xaxis": {"automargin": True},
                        "yaxis": {"automargin": True},
                        "height": 250,
                        "margin": {"t": 50, "l": 10, "r": 10},
                        "title": column
                    },
                },
            )
            for column in ["score", "word density", "unique density"]
        ]
    )
@app.callback(Output('table', 'data'),
              [Input('refresh', 'n_clicks')],
              [State('input-1-state', 'value')
               ])
def update_data(n_clicks, subreddits):
    dff = df
    if subreddits is None:
        subreddits = 'wallstreetbets'
    else:
        subreddits
    if n_clicks is None:
        raise PreventUpdate
    else:
        posts = []
        new_bets = reddit.subreddit(subreddits).hot(limit=100)
        for post in new_bets:
            posts.append([post.title, post.score, post.num_comments, post.selftext, post.created, post.pinned, post.total_awards_received])
        posts = pd.DataFrame(posts,columns=['title', 'score', 'comments', 'post', 'created', 'pinned', 'total awards'])
        dff = get_features(posts)
    
    return dff.to_dict('records')
if __name__ == '__main__':
    app.run_server(debug=True)