{{ title }}
{{ subtitle }}
+ + {% endif %}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} ++
diff --git a/apps/viz/apps.py b/apps/viz/apps.py new file mode 100644 index 00000000..e7a99a2e --- /dev/null +++ b/apps/viz/apps.py @@ -0,0 +1,12 @@ +# viz/apps.py +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class VizConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "viz" + verbose_name = _("viz") + + def ready(self): + import viz.dash.inventory_dashboard.app # noqa diff --git a/apps/viz/dash/inventory_dashboard/__init__.py b/apps/viz/dash/inventory_dashboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/viz/dash/inventory_dashboard/app.py b/apps/viz/dash/inventory_dashboard/app.py new file mode 100644 index 00000000..c8e142d6 --- /dev/null +++ b/apps/viz/dash/inventory_dashboard/app.py @@ -0,0 +1,206 @@ +import dash_bootstrap_components as dbc +import pandas as pd +import plotly.express as px +from dash import dash_table, dcc, html +from dash.dependencies import Input, Output +from django_plotly_dash import DjangoDash + +from .functions import clean_livelihood_data, clean_wealth_group_data + +# Unique countries and livelihood zones for dropdowns +unique_countries = sorted(clean_livelihood_data["country_code"].dropna().unique()) +unique_zones = sorted(clean_livelihood_data["livelihood_zone_baseline_label"].dropna().unique()) + +# Dash app +app = DjangoDash("Inventory_dashboard", suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.BOOTSTRAP]) +app.title = "HEA Dashboard" + +# Layout +app.layout = html.Div( + [ + html.H1("HEA Data Inventory Dashboard", style={"textAlign": "center"}), + html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="country-dropdown", + options=[{"label": country, "value": country} for country in unique_countries], + placeholder=f"Select Country(s) (e.g., {unique_countries[0]})", + multi=True, + ), + ], + style={"flex": "1", "marginRight": "10px"}, + ), + html.Div( + [ + dcc.Dropdown( + id="livelihood-zone-dropdown", + options=[{"label": zone, "value": zone} for zone in unique_zones], + placeholder=f"Select Livelihood Zone(s) (e.g., {unique_zones[0]})", + multi=True, + ), + ], + style={"flex": "1"}, + ), + ], + style={ + "display": "flex", + "width": "100%", + "justifyContent": "space-between", + "marginBottom": "20px", + }, + ), + html.Div( + [ + html.Div( + dcc.Graph(id="wealth-chart"), + style={"width": "30%", "display": "inline-block"}, + ), + html.Div( + dcc.Graph(id="livelihood-chart"), + style={"width": "30%", "display": "inline-block"}, + ), + html.Div( + dcc.Graph(id="wealth-monthly-chart"), + style={"width": "30%", "display": "inline-block"}, + ), + ], + ), + html.Div( + [ + html.H3("Data Table", style={"textAlign": "center"}), + dash_table.DataTable( + id="data-table", + columns=[ + {"name": "Livelihood Zone", "id": "livelihood_zone_baseline_label"}, + {"name": "Total Strategy Type Count", "id": "Strategy Type Count"}, + {"name": "Summary Wealth Characteristics", "id": "Summary Data"}, + {"name": "Community Wealth Characteristics", "id": "Community Data"}, + ], + style_table={"overflowX": "auto"}, + style_cell={ + "textAlign": "left", + "fontSize": "15px", # Increase font size + "padding": "10px", # Add padding + }, + style_header={ + "fontSize": "18px", # Increase font size for header + "fontWeight": "bold", # Make header bold + "textAlign": "center", + }, + page_size=10, + ), + ], + className="inventory-filter inventory-filter-last", + ), + ], + className="div-wrapper control-panel-wrapper", +) + + +# Callbacks +@app.callback( + [ + Output("livelihood-zone-dropdown", "options"), + Output("wealth-chart", "figure"), + Output("livelihood-chart", "figure"), + Output("wealth-monthly-chart", "figure"), + Output("data-table", "data"), + ], + [Input("country-dropdown", "value"), Input("livelihood-zone-dropdown", "value")], +) +def update_charts(selected_countries, selected_zones): + # Handle multi-country selection + if selected_countries: + filtered_livelihood = clean_livelihood_data[clean_livelihood_data["country_code"].isin(selected_countries)] + filtered_wealth = clean_wealth_group_data[clean_wealth_group_data["country_code"].isin(selected_countries)] + filtered_zones = sorted(filtered_livelihood["livelihood_zone_baseline_label"].unique()) + else: + filtered_livelihood = clean_livelihood_data + filtered_wealth = clean_wealth_group_data + filtered_zones = unique_zones + + # Update options for livelihood zone dropdown + zone_options = [{"label": zone, "value": zone} for zone in filtered_zones] + + # Filter data based on selected livelihood zones + if selected_zones: + filtered_livelihood = filtered_livelihood[ + filtered_livelihood["livelihood_zone_baseline_label"].isin(selected_zones) + ] + filtered_wealth = filtered_wealth[filtered_wealth["livelihood_zone_baseline_label"].isin(selected_zones)] + + # Group and aggregate Strategy Type Count + livelihood_grouped = ( + filtered_livelihood.groupby(["livelihood_zone_baseline_label", "strategy_type_label"]) + .size() + .reset_index(name="Strategy Type Count") + ) + livelihood_summary = ( + livelihood_grouped.groupby("livelihood_zone_baseline_label")["Strategy Type Count"].sum().reset_index() + ) + + # Group and pivot Wealth Characteristics Count + wealth_grouped = ( + filtered_wealth.groupby(["livelihood_zone_baseline_label", "Record Type"]) + .size() + .reset_index(name="Wealth Characteristics Count") + ) + wealth_grouped_pivot = wealth_grouped.pivot_table( + index="livelihood_zone_baseline_label", + columns="Record Type", + values="Wealth Characteristics Count", + aggfunc="sum", + fill_value=0, + ).reset_index() + + # Merge livelihood and wealth data + merged_data = pd.merge(livelihood_summary, wealth_grouped_pivot, on="livelihood_zone_baseline_label", how="left") + + # Create charts + wealth_fig = px.bar( + wealth_grouped, + x="livelihood_zone_baseline_label", + y="Wealth Characteristics Count", + color="Record Type", + barmode="stack", + title="Wealth Characteristics by Type", + labels={ + "livelihood_zone_baseline_label": "Livelihood Zone", + }, + ) + + livelihood_fig = px.bar( + livelihood_grouped, + x="strategy_type_label", + y="Strategy Type Count", + color="livelihood_zone_baseline_label", + barmode="stack", + title="Strategy Types by Baseline", + labels={ + "strategy_type_label": "Strategy Type", + "livelihood_zone_baseline_label": "Baseline Zone", + }, + ) + + wealth_monthly_fig = px.bar( + filtered_wealth.groupby(["created_month", "Record Type"]) + .size() + .reset_index(name="Wealth Characteristics Count"), + x="created_month", + y="Wealth Characteristics Count", + color="Record Type", + barmode="stack", + title="Wealth Characteristics by Month", + labels={ + "created_month": "Month", + }, + ) + + return zone_options, wealth_fig, livelihood_fig, wealth_monthly_fig, merged_data.to_dict("records") + + +# Run the app +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/apps/viz/dash/inventory_dashboard/functions.py b/apps/viz/dash/inventory_dashboard/functions.py new file mode 100644 index 00000000..0f126b72 --- /dev/null +++ b/apps/viz/dash/inventory_dashboard/functions.py @@ -0,0 +1,67 @@ +import pandas as pd +import requests + +# API Endpoints +LIVELIHOOD_STRATEGY_URL = "https://headev.fews.net/api/livelihoodstrategy/" +WEALTH_GROUP_URL = "https://headev.fews.net/api/wealthgroupcharacteristicvalue/" +LIVELIHOOD_ACTIVITY_URL = "https://headev.fews.net/api/livelihoodactivity/" + + +def fetch_data(api_url): + """ + Fetch data from the given API endpoint and return as a Pandas DataFrame. + """ + try: + response = requests.get(api_url) + response.raise_for_status() + data = response.json() + return pd.DataFrame(data) + except Exception as e: + print(f"Error fetching data: {e}") + return pd.DataFrame() + + +def prepare_livelihood_data(df): + """ + Prepare livelihood strategy data for visualization. + """ + df.rename(columns={"livelihood_zone_country": "country_code"}, inplace=True) + df["ls_baseline_date"] = df["livelihood_zone_baseline_label"].str.split(": ").str[1] + df["ls_baseline_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month + return df + + +def prepare_wealth_group_data(df): + """ + Prepare wealth group data for visualization. + """ + df.rename(columns={"livelihood_zone_country_code": "country_code"}, inplace=True) + + if "livelihood_zone_baseline_label" in df.columns: + df["ls_baseline_date"] = df["livelihood_zone_baseline_label"].str.split(": ").str[1] + else: + df["ls_baseline_date"] = None + + df["ls_baseline_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month + + month_mapping = {month: pd.Timestamp(f"2023-{month:02}-01").strftime("%B") for month in range(1, 13)} + + if "created_date" in df.columns: + df["created_month"] = pd.to_datetime(df["created_date"], errors="coerce").dt.month.map(month_mapping) + else: + df["created_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month.map(month_mapping) + + if "community_name" in df.columns: + df["Record Type"] = df["community_name"].apply(lambda x: "Summary Data" if pd.isna(x) else "Community Data") + else: + df["Record Type"] = "Community Data" + + return df + + +# Fetch and prepare data +livelihood_data = fetch_data(LIVELIHOOD_STRATEGY_URL) +wealth_group_data = fetch_data(WEALTH_GROUP_URL) + +clean_livelihood_data = prepare_livelihood_data(livelihood_data) +clean_wealth_group_data = prepare_wealth_group_data(wealth_group_data) diff --git a/apps/viz/dash_wrapper.py b/apps/viz/dash_wrapper.py new file mode 100644 index 00000000..b02bc190 --- /dev/null +++ b/apps/viz/dash_wrapper.py @@ -0,0 +1,16 @@ +from django_plotly_dash import DjangoDash + + +class SecureDjangoDash(DjangoDash): + """ + An extended version of DjangoDash that allows fine-grained control of permissions + and clickjacking protection + """ + + xframe_options = "DENY" # None (i.e. Allow) or SAMEORIGIN or DENY + login_required = True + permission_required = None + + def __init__(self, *args, **kwargs): + self.permission_required = kwargs.pop("permission_required", None) + super().__init__(*args, **kwargs) diff --git a/apps/viz/static/viz/css/dashboards/dash_base.css b/apps/viz/static/viz/css/dashboards/dash_base.css new file mode 100644 index 00000000..4c70f6c2 --- /dev/null +++ b/apps/viz/static/viz/css/dashboards/dash_base.css @@ -0,0 +1,223 @@ +#content { + font-family: "Segoe UI", "Open Sans", Arial, sans-serif !important; + color: #212721 !important; + background-color: #e6e7e8; + /* font-size:18px !important; */ +} + +.div-wrapper { + display: flex; + border-radius: 5px; + background-color: #fff; + padding: 10px; + box-shadow: 1px 1px 1px #bec1c3; + -webkit-transition: box-shadow 0.1s ease-in-out 0.2s; + -moz-transition: box-shadow 0.1s ease-in-out 0.2s; + -o-transition: box-shadow 0.1s ease-in-out 0.2s; + transition: box-shadow 0.1s ease-in-out 0.2s; + /* border: 2px solid #D1D3D4; */ +} + +.div-wrapper:hover { + box-shadow: 2px 2px 7px #9fa1a1; +} + +.dash-spreadsheet-inner { + padding: 0 15px; +} + +/* Add class to all chart headers */ +.chart-header { + font-weight: 700; + font-size: 28px; + text-align: left; + padding-bottom: 10px; + font-family: "Georgia", "Tinos", serif; +} + +/* Add class to all sub headers */ +.sub-header { + text-align: start; + padding-right: 10px; +} + +.sub-header-vertic { + text-align: start; + padding-bottom: 10px; + padding-top: 15px; +} + +.information-icon { + font-size: 36px; + font-weight: 900; + color: #096640; +} + +.information-wrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.modal { + position: fixed; + z-index: 10000; + left: 0; + top: 0px; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); +} + +.modal-content { + width: 50%; + top: 150px; + margin: auto; + padding: 30px; + background-color: white; +} + +.modal-content li, +.modal-content h4 { + text-align: left; +} + +/* Add styles to dropdowns */ +.Select-control { + border: none !important; +} + +.Select { + border: 1px solid #7c888d !important; + /* height: 40px !important; */ + color: #585858 !important; + border-radius: 5px; +} + +.Select-control { + height: 34px; +} + +.Select:hover, +.Select.is-focused { + border: 1px solid #096640 !important; +} + +.Select-placeholder { + /* margin-top: 8px; */ + color: #585858 !important; +} + +.VirtualizedSelectOption { + color: #212721; + font-weight: bold; +} + +.VirtualizedSelectFocusedOption { + background-color: #bddbd6; +} + +.Select-value-label, +.Select-value-icon { + /* background-color: #BDDBD6; */ + background-color: #00000000 !important; + color: #212721; + border: 0px solid #00000000 !important; +} + +.has-value.Select--single>.Select-control .Select-value .Select-value-label { + color: #212721; +} + +/* .Select--single>.Select-control .Select-value { + top: 10px; +} */ + +.Select--multi .Select-value { + /* margin-top: 10px !important; */ + background-color: #00000000 !important; + color: #212721 !important; + border: 1px solid #bddbd6 !important; +} + +#search_filter::placeholder, +input.DateInput_input.DateInput_input_1::placeholder, +input.DateInput_input.DateInput_input_1 { + font-size: 14px; + font-family: "Segoe UI", "Open Sans", Arial, sans-serif; + color: #585858; +} + +input.DateInput_input.DateInput_input_1 { + color: #212721; +} + +.submit-button { + padding: 5px; + margin-bottom: 15px; + margin-top: 25px; + font-size: 18px; + border: 1px solid #096640; + background-color: #096640; + color: #ffffff; +} + +.submit-button2 { + padding: 5px; + margin-bottom: 15px; + margin-top: 25px; + font-size: 18px; + border: 1px solid #096640; + background-color: #bddbd6; + color: #212721; +} + +/* Dash table radio and checkbox input styling */ +.dash-table-container .dash-spreadsheet-container input[type="radio"], +.dash-table-container .dash-spreadsheet-container input[type="checkbox"] { + border: 1px solid #212721; + color: #096640; + font-weight: bold; + width: 15px; + height: 15px; +} + +.dash-table-container .dash-spreadsheet-container input[type="radio"] { + border-radius: 50%; + position: relative; + margin-right: 5px; +} + +.dash-table-container .dash-spreadsheet-container input[type="radio"]:checked:before { + content: ""; + background-color: #096640; + float: left; + height: 100%; + width: 100%; + position: absolute; + transform: scale(0.65); + border-radius: 50%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; +} + +.dash-table-container .dash-spreadsheet-container input[type="checkbox"]:checked:before { + content: ""; + background-color: #096640; + border-radius: 5%; + float: left; + width: 15px; + height: 15px; + text-align: center; +} + +/* Plotly annotations rendered in a separate div */ +.plotly-chart-annotation { + font-weight: 300; + font-size: 16px; + text-align: right; +} + +#header ul#navigation-menu li a { + text-transform: none !important; +} \ No newline at end of file diff --git a/apps/viz/static/viz/css/dashboards/inventory_dashboard.css b/apps/viz/static/viz/css/dashboards/inventory_dashboard.css new file mode 100644 index 00000000..276afdc2 --- /dev/null +++ b/apps/viz/static/viz/css/dashboards/inventory_dashboard.css @@ -0,0 +1,92 @@ +.dashboard-header { + text-align: center; + margin-bottom: 20px; +} + +.dropdown-row { + display: flex; + justify-content: space-between; + width: 100%; + margin-bottom: 20px; +} + +.dropdown-item { + flex: 1; + margin-right: 10px; +} + +.dropdown-item:last-child { + margin-right: 0; +} + +.graph-container { + display: flex; + justify-content: space-between; + width: 100%; +} + +.graph-item { + width: 48%; +} + + +.control-panel-wrapper { + grid-area: control-panel-wrapper; +} + +.control-panel2-wrapper { + grid-area: control-panel2-wrapper; +} + +.main-wrapper { + margin: 0px; + padding: 0; + display: grid; + gap: 10px; + grid-template-columns: 1fr; + grid-template-rows: auto; + grid-template-areas: + "control-panel-wrapper" + "scatter-map-row-wrapper" + "control-panel2-wrapper" + "choropleth-map-wrapper" + ; +} + + +.control-panel-wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.inventory-filter { + width: 100%; + padding-right: 5px; +} + +.inventory-filter-last { + padding-right: 0px; +} + +.filters-wrapper { + display: flex; +} + +.dash-graph { + width: 100%; +} + +.table-wrapper { + display: flex; + flex-direction: column; +} + +.one-third-div { + width: 32%; + box-shadow: none; + transition: box-shadow none; + -webkit-transition: box-shadow none; + -moz-transition: box-shadow none; +} + diff --git a/apps/viz/templates/dashboard/dashboard_inventory_dashboard.html b/apps/viz/templates/dashboard/dashboard_inventory_dashboard.html new file mode 100644 index 00000000..2c36a1d4 --- /dev/null +++ b/apps/viz/templates/dashboard/dashboard_inventory_dashboard.html @@ -0,0 +1,7 @@ +{%extends "dashboard/internal_dashboard.html"%} +{% load static%} + +{% block styles %} +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/apps/viz/templates/dashboard/internal_dashboard.html b/apps/viz/templates/dashboard/internal_dashboard.html new file mode 100644 index 00000000..53e68494 --- /dev/null +++ b/apps/viz/templates/dashboard/internal_dashboard.html @@ -0,0 +1,39 @@ +{% extends "admin/base.html" %} +{% load i18n static plotly_dash %} +{% load static %} + +{% block extrahead %} + {% block app_header_css %} + {% endblock app_header_css %} + {% plotly_header %} + + {% block styles %} + + + + {% include "snippets/css_deferred.html" with href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Tinos:ital,wght@0,400;0,700;1,400;1,700&display=swap" only %} + + {% static 'viz/css/dashboards/dash_base.css' as dash_base_css_href %} + {% include "snippets/css_deferred.html" with href=dash_base_css_href only %} + {% endblock %} + +{% endblock extrahead %} + +{% block content %} + + {% block content_main %} +
{{ value }}
\ No newline at end of file diff --git a/apps/viz/templates/viz/blocks/visualization.html b/apps/viz/templates/viz/blocks/visualization.html new file mode 100644 index 00000000..d5dd3075 --- /dev/null +++ b/apps/viz/templates/viz/blocks/visualization.html @@ -0,0 +1,3 @@ +`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don\'t allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don\'t allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don\'t allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `