Listing with map view
Page that shows some KPIs and a list of results alongside a map.
Dataset in use: fr-esr-principaux-etablissements-enseignement-superieur
(See it on mesr domain)
Fields in use:
type_d_etablissement | secteur_d_etablissement | dep_nom | adresse_uai | dep_nom | numero_telephone_uai | url | uo_lib | geo_point |
---|---|---|---|---|---|---|---|---|
École | Public | Rhône | 36 avenue Guy de Collongue | Rhône | 472186000 | https://www.ec-lyon.fr/ | Centrale Lyon | 48.85535,2.345259 |
École | Public | Vienne | 1 avenue Clément Ader | Vienne | 549498080 | https://www.ensma.fr/ | École nationale supérieure de | 48.85535,2.345259 |
École | Privé | Val-de-Marne | 79 avenue Aristide Briand | Val-de-Marne | 181801515 | https://www.esitc-paris.fr/fr | École supérieure d’ingénieurs des travaux de la construction de Paris | 48.85535,2.345259 |
École | Privé | Seine-et-Marne | Boulevard de Constance | Seine-et-Marne | 160724000 | https://www.insead.edu/ | Institut européen d’administration des affaires | 48.861613,2.336221 |
<!-- V1.2 :
- Add fieldImage and imagePosition for image datasets
- Add mapPictoColor and mapPicto to set up map picto
-->
<!-- V1.1 :
- Add ods-select and multiple choice option for filters
- Add clear all filter button when one filter is applied
- responsive display for filters and date button
- KPI default value to 0 when no data/results to display
-->
<!-- IMPORTANT ******** MUST READ !
In the following settings declaration :
A common error is forget to escape (protect) apostrophe with a leading backslash
As apostrophes are used to declare values of variable it will break the settings
Ex:
wrongVariable = 'I'll be freed from apostrophes'
correctVariable = 'I\'ll be freed from apostrophes'
-->
<!-- SETTINGS START HERE -->
<div class="container"
ng-init="domain = 'mesr.opendatasoft.com';
datasetid = 'fr-esr-principaux-etablissements-enseignement-superieur';
filters = [
{'id':'type_d_etablissement','multiple':true},
{'id':'secteur_d_etablissement','multiple':false},
{'id':'dep_nom','multiple':true}
];
resetFiltersButton = true;
resetFiltersButtonLabel = 'Supprimer tous les filtres';
fieldsList = ['adresse_uai', 'dep_nom', 'numero_telephone_uai'];
fieldLink = 'url';
fieldLinkLabel = 'Visiter le site web';
fieldImage = '';
imagePosition = 'left';
resultTitle = 'uo_lib';
itemsPerRow = '2';
kpis = [
{
'title': 'Nombre d\'établissements',
'function': 'COUNT',
'faicon': 'graduation-cap'
}
];
headerBackgroundImage = 'https://images.unsplash.com/photo-1541890289-b86df5bafd81?ixlib=rb-1.2.1&auto=format&fit=crop&w=2828&q=80';
subtitle = 'Établissements près de chez vous';
mapPictoColor = '';
mapPicto = '';
DO_NOT_MODIFY_BELOW;
ctxfields = {};
values = {};
activeFilters = {};
">
<!-- ### GENERAL SETTINGS ### -->
<!-- domain : (Domain URL) : Must contain the ID of the domain where the dataset is published.
ex: 'discovery.opendatasoft.com'
-->
<!-- datasetid (Dataset ID) : Must contain the ID of the dataset
ex: 'oeuvres-de-johannes-vermeer'
-->
<!-- headerBackgroundImage : Use an URL to show a background image in the header.
-->
<!-- ### FILTERS SETTINGS ### -->
<!-- filters (Filters) : List of object that contains the IDs to generate the filters pannel.
and multiple true or false to allow the user to select multiple values in the filter.
NB: the field must be a facet in the dataset
NB: alphanumerical sort is applied in the filter view
ex: [
{'id':'filterid','multiple':true},
{'id':'filterid2','multiple':false}
]
-->
<!-- resetFiltersButton (boolean) : add a reset filters button after filters block -->
<!-- resetFiltersButtonLabel (Label of the button) : test to display when a filter is selected
ex: 'Clear all filters'
-->
<!-- ### LIST VIEW SETTINGS ### -->
<!-- fieldsList (List configuration) : Set the list of field IDs
ex: ['title','category','genre','date']
-->
<!-- fieldLink (Link to an external resource) : If available, the field ID of some external resource as a web URL
ex: 'link'
-->
<!-- fieldLinkLabel (The label of that link) : Label of the link button
ex: 'Read more here'
-->
<!-- fieldImage (Field id of the image field if any)
ex: 'image'
-->
<!-- imagePosition (Image position) : Image position in the card, can be 'top' or 'left'
ex: 'left'
-->
<!-- ### KPIS SETTINGS ### -->
<!-- KPIS settings is a list of object that describes each KPI
List of available keys are :
- title (Name of the KPI) ex: 'Average # of citizens'
- function (function of the aggregation) ex: 'SUM'
- expression (field id that contains numerical values to aggregate) ex: 'population'
- precision (Decimal precision of the KPI) ex: 2
- unit (KPI unit) ex: 'citizens'
- faicon (FontAwesome icon id) ex: 'square-o'
title, function, expression are MANDATORY
the others are optionnal
Available functions are SUM, AVG, COUNT, STD, MAX, MIN.
Please see the documentation for more information
https://help.opendatasoft.com/widgets/#/api/ods-widgets.directive:odsAggregation
Please see all available icons here
https://fontawesome.com/v4.7.0/icons/
ex:
kpis = [
{
'title': 'Taille moyenne',
'function': 'AVG',
'expression': 'surface',
'precision': 2,
'unit': 'm2',
'faicon': 'square-o'
},
{
'title': 'Nombre d\'oeuvre référencées',
'function': 'COUNT'
}
];
-->
<!-- ### Subtitle ### -->
<!-- subtitle : "Some texte" -->
<!-- mapPictoColor = '#EC643C'
mapPicto = 'ods-photo'
Map picto settings to set the color and the picto from this list : https://help.opendatasoft.com/platform/en/other_resources/pictograms_reference/pictograms_reference.html
-->
<!-- DO NOT MODIFY -->
<!-- Technical fields, do not modify please -->
<ods-dataset-context context="ctx"
ctx-domain="{{ domain }}"
ctx-dataset="{{ datasetid }}">
<!-- Private datasets can be accessed by adding an API Key.
Add this param to the <ods-dataset-context above,
where XXX is your apikey :
ctx-apikey="XXX"
-->
<div class="page-header" style="background-image: url({{ headerBackgroundImage }})">
<h1 class="page-title">
{{ ctx.dataset.metas.title }}
</h1>
</div>
<!-- Page subtitle -->
<h2 class="page-subtitle" ng-bind-html="ctx.dataset.metas.description | shortSummary"></h2>
<span ng-repeat="field in ctx.dataset.fields">
{{ ctxfields[field.name] = field.label; '' }}
</span>
<span ng-repeat="filter in filters">
{{ ctx.parameters['refine.' + filter.id] = activeFilters[filter.id] ; '' }}
</span>
<!-- KPIs -->
<section class="kpis-container row row-equal-height">
<div class="{{ 'col-md-' + (12/itemsPerRow) }} margin-bottom-20"
ng-repeat="kpi in kpis">
<!-- KPI box component -->
<div class="kpi-card"
ods-aggregation="agg"
ods-aggregation-context="ctx"
ods-aggregation-function="{{ kpi.function }}"
ods-aggregation-expression="{{ kpi.expression }}">
<i class="kpi-icon fa fa-{{ kpi.faicon || 'gitlab' }}" aria-hidden="true"></i>
<p class="kpi-title">
{{ (agg || 0) | number : (kpi.precision || 0) }}
<span ng-if="kpi.unit" class="kpi-unit">{{ kpi.unit }}</span>
</p>
<p class="kpi-description">
{{ kpi.title }}
</p>
</div>
</div>
</section>
<!-- Section 1 -->
<section class="page-section">
<h2 class="section-title">
{{ subtitle }}
</h2>
<div class="content-card">
<p class="content-card-description">
Utilisez les filtres ci-dessus pour affiner votre recherche.
</p>
<!-- SEARCH -->
<div class="search-module">
<i class="fa fa-search search-module-icon" aria-hidden="true"></i>
<input placeholder="Rechercher"
ng-model="ctx.parameters['q']"
ng-model-options="{ updateOn: 'keyup', debounce: { 'default': 300, 'blur': 0 }}"
class="search-module-input"
type="text"/>
<button class="search-module-clear"
ng-if="ctx.parameters['q']"
ng-click="ctx.parameters['q'] = undefined">
<i class="fa fa-times-circle" aria-hidden="true"></i>
</button>
</div>
<!-- FILTERS -->
<div class="filter-list"
ng-init="dropdown.open = '';
select = {}">
<div ng-repeat="filter in filters">
{{ ctx.parameters['disjunctive.' + filter.id] = true; '' }}
<div ods-facet-results="categories"
ods-facet-results-facet-name="{{ filter.id }}"
ods-facet-results-context="ctx"
ods-facet-results-sort="alphanum">
<ods-select ng-if="ctxfields[filter.id]"
selected-values="activeFilters[filter.id]"
multiple="filter.multiple"
options="categories"
label-modifier="name"
value-modifier="name"
placeholder="{{ ctxfields[filter.id] }}"></ods-select>
</div>
</div>
<div class="clear-filters"
ng-show="(activeFilters | values).join('')">
<div class="clear-filters-button"
role="button"
ng-click="activeFilters = {}">
{{ resetFiltersButtonLabel }}
<i class="fa fa-times-circle" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="row">
<div class="col-md-5 col-xs-12">
<ul class="result-list">
<li class="result"
ng-repeat="item in items"
ods-results="items"
ods-results-context="ctx"
ods-results-max="1000"
ng-click="ctx.parameters['refine.' + resultTitle] = (ctx.parameters['refine.' + resultTitle]?undefined:item.fields[resultTitle])"
ng-class="{'result-img-horizontal': imagePosition === 'left' }">
<div class="result-img"
ng-if="item.fields[fieldImage]"
style="{{ 'background-image: url(https://' + domain + '/explore/dataset/' + datasetid + '/files/' + item.fields[fieldImage].id + '/300/);' }}">
</div>
<div class="result-content">
<h2 class="result-title">
{{ item.fields[resultTitle] }}
</h2>
<dl class="result-info">
<dt ng-repeat-start="field in fieldsList">
{{ ctxfields[field] }}
</dt>
<dd ng-repeat-end>{{ item.fields[field] }}</dd>
</dl>
<div ng-if="fieldLink">
<!-- fieldLink is used here
For very simple usage, just set the href to the field value :
- href="{{ item.fields[fieldLink] }}"
For more advanced scenario, you can send the user to :
- the dataset table filtered with the fieldLink value, through a text query
href="/explore/dataset/{{ datasetid }}/table?q={{ item.fields[fieldLink] }}"
- the dataset table filtered with the fieldLink value, through a refine on the field
href="/explore/dataset/{{ datasetid }}/table?refine.{{ fieldLink }}={{ item.fields[fieldLink] }}"
- a page using url-sync=true setting :
href="/pages/yourpage/?refine.{{ fieldLink }}={{ item.fields[fieldLink] }}"
-->
<a href="{{ 'https://' + domain + '/explore/dataset/' + datasetid + '/files/' + item.fields[fieldLink].id + '/300/' }}"
target="_blank"
class="content-card-button"
ng-if="item.fields[fieldLink]">
{{ fieldLinkLabel }}
</a>
</div>
</div>
</li>
</ul>
</div>
<div class="col-md-7 col-xs-12">
<ods-map no-refit="false"
scroll-wheel-zoom="false">
<ods-map-layer-group>
<ods-map-layer context="ctx"
color="{{ mapPictoColor }}"
picto="{{ mapPicto }}"
show-marker="true"
display="auto"
shape-opacity="0.5"
point-opacity="1"
border-color="#FFFFFF"
border-opacity="1"
border-size="1"
border-pattern="solid"
size="4"
size-min="3"
size-max="5"
size-function="linear"></ods-map-layer>
</ods-map-layer-group>
</ods-map>
</div>
</div>
</div>
<a href="{{ ctx.domainUrl }}/explore/dataset/{{ datasetid }}" target="_blank">Accéder aux données source</a>
</section>
</ods-dataset-context>
</div>
</div>
/* General Layout
========================================================================== */
main.main--page {
margin: 0;
}
/* Body text */
p {
text-align: justify;
margin-top: 0;
margin-bottom: 1.5rem;
}
ul {
list-style: none;
padding-left: 0;
}
@media screen and (min-width: 992px) {
.row-equal-height {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-bottom: 20px;
}
/* Fix for early content wrapping in Safari*/
.row-equal-height:before,
.row-equal-height:after {
content: normal;
}
}
.margin-bottom-20 {
margin-bottom: 20px;
}
.page-section {
margin-top: 2rem;
margin-bottom: 6rem;
}
.section-title {
font-size: 2.2rem;
font-weight: 400;
color: #FFF;
background-color: var(--highlight);
text-align: center;
padding: 10px;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
/* Header
========================================================================== */
.page-header {
background-color: var(--highlight);
min-height: 180px; /* Change the height of the image here by increasing or decreasing the pixels. */
/* Properties of the title located within the image banner */
display: flex;
justify-content: center;
align-items: flex-end;
margin-bottom: 40px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
background-image: url('');
}
.page-title {
color: var(--highlight);
text-align: center;
background-color: var(--page-background);
position: relative;
vertical-align: bottom;
margin: 0;
font-weight: 400;
font-size: 2.5rem;
padding: 15px 20px 10px 20px;
border-radius: 7px 7px 0px 0px;
}
.page-subtitle {
font-weight: 400;
margin-top: 0;
margin-bottom: 20px;
font-size: 1.14rem;
}
/* Search Module
========================================================================== */
.search-module {
display: flex;
align-items: stretch;
border-bottom: 1px solid #dee5ef;
margin-bottom: 13px;
transition: all .2s;
}
.search-module:hover,
.search-module:focus-within {
border-bottom-color: var(--links);
}
.search-module-icon {
color: #898d92;
margin-right: 8px;
align-self: center;
}
.search-module-input {
background-color: transparent;
width: 100%;
outline: none;
border: none;
padding: 12px 0;
transition: all .2s;
color: var(--text);
}
.search-module-input::placeholder {
transition: all .2s;
}
.search-module-clear {
color: #898d92;
font-size: 1rem;
background: transparent;
border: none;
margin: 0;
outline: none;
padding: 0 0 0 12px;
transition: all .2s;
}
.search-module-clear:hover {
opacity: .65;
}
.search-module:hover .search-module-icon,
.search-module:focus-within .search-module-icon,
.search-module:hover .search-module-input::placeholder,
.search-module:focus-within .search-module-input::placeholder {
color: var(--links)
}
/* Filters
========================================================================== */
.filter-list {
display: flex;
flex-wrap: wrap;
position: relative;
}
.filter-list > * {
margin: 0 0 10px;
width: 100%;
}
.odswidget-select .odswidget-select-dropdown.open .odswidget-select-dropdown-menu {
width: 100%
}
.clear-filters {
display: flex;
align-items: center;
justify-content: center;
}
.clear-filters-button:hover {
opacity: 0.65;
}
.odswidget-select,
.odswidget-select .odswidget-select-dropdown {
width: 100%;
}
@media screen and (min-width: 500px) {
.filter-list > * {
margin: 0 10px 10px 0;
width: inherit;
}
.odswidget-select .odswidget-select-dropdown.open .odswidget-select-dropdown-menu {
width: max-content;
min-width: 240px;
}
}
/* Content Card
========================================================================== */
.content-card {
background-color: var(--boxes-background);
border-radius: 4px;
height: 100%;
padding: 26px;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.13);
margin-bottom: 10px;
}
.content-card-description {
color: var(--text);
font-size: 1rem;
line-height: 1.5;
font-weight: normal;
margin-top: 0;
margin-bottom: 26px;
max-width: 100%;
}
.content-card-button {
color: var(--highlight);
border: 1px solid var(--highlight);
background: transparent;
display: inline-block;
text-align: center;
font-size: .867rem;
border-radius: 4px;
padding: .5rem 1.15rem;
text-decoration: none;
transition: all .2s;
}
.content-card-button:hover {
background-color: var(--highlight);
color: #FFFFFF;
text-decoration: none;
}
/* KPI Card
========================================================================== */
@media screen and (min-width: 992px) {
.kpis-container {
display: flex;
justify-content: center;
}
}
.kpi-card {
background-color: var(--boxes-background);
height: 100%;
padding: 39px;
border-radius: 4px;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.13);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.kpi-icon {
color: var(--highlight);
color: var(--secondary-color);
font-size: 4rem;
margin-top: 0;
margin-bottom: 13px;
max-width: 100%;
}
.kpi-title {
font-weight: normal;
color: var(--highlight);
font-size: 3.2rem;
margin-top: 0;
margin-bottom: 13px;
max-width: 100%;
}
.kpi-unit {
font-size: 0.8em;
color: var(--secondary-color);
}
.kpi-description {
color: var(--text);
font-size: 1rem;
line-height: 1.5;
font-weight: normal;
margin-top: 0;
margin-bottom: 0;
max-width: 100%;
}
/* Results and Map
========================================================================== */
.odswidget-map__map {
min-height: 550px;
}
@media screen and (min-width: 991px) {
.col-md-5 {
padding-right: 0;
}
.col-md-7 {
padding-left: 0;
}
}
.result-list {
list-style: none;
padding-left: 0;
margin-top: 0;
height: 550px;
overflow-y: auto;
}
.result {
margin-bottom: 10px;
padding: 0 1rem 1rem 0;
border-bottom: 1px solid #DEE5EF;
}
.result.result-img-horizontal {
display: flex;
}
.result-img-horizontal .result-img {
flex: 0 0 35%;
width: 35%;
height: auto;
}
.result-img {
disaply: block;
height: 118px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
.result-img-horizontal .result-content {
margin-left: 0.7rem;
}
:not(.result-img-horizontal) .result-content {
margin-top: 0.7rem;
}
.result-title {
color: var(--titles);
font-size: 1rem;
font-weight: bold;
margin-top: 0;
margin-bottom: 5px;
}
.result-info {
list-style: none;
padding-left: 0;
margin-top: 0;
margin-bottom: 10px;
overflow-wrap: break-word;
}
.result-info dt {
font-size: .9rem;
opacity: .9;
}
.result-info dd {
margin-left: 0;
margin-bottom: 5px;
}