├── .babelrc ├── .github ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .jscsrc ├── LEAD.md ├── LICENSE ├── Makefile ├── README.md ├── app ├── backend │ ├── controllers.js │ ├── index.js │ ├── middlewares.js │ ├── routes.js │ ├── services.js │ └── views │ │ ├── dashboard.jsx │ │ ├── default.jsx │ │ └── page.jsx ├── bootstrap.js ├── config.js ├── content │ ├── about.md │ └── faq.md ├── index.js ├── ui │ ├── scripts │ │ ├── actions │ │ │ └── index.js │ │ ├── components │ │ │ ├── charts │ │ │ │ ├── index.js │ │ │ │ ├── main.js │ │ │ │ ├── publisher.js │ │ │ │ └── source.js │ │ │ ├── overviews │ │ │ │ ├── index.js │ │ │ │ ├── main.js │ │ │ │ └── publisher.js │ │ │ └── tables │ │ │ │ ├── filter.js │ │ │ │ ├── head.js │ │ │ │ ├── index.js │ │ │ │ ├── info.js │ │ │ │ ├── resize.js │ │ │ │ └── table.js │ │ ├── containers │ │ │ ├── App.js │ │ │ ├── Embed.js │ │ │ ├── Main.js │ │ │ └── Publisher.js │ │ ├── index.js │ │ ├── reducers │ │ │ └── index.js │ │ ├── store │ │ │ └── configureStore.js │ │ └── utils │ │ │ ├── calc.js │ │ │ ├── index.js │ │ │ └── ui.js │ └── styles │ │ ├── _footer.scss │ │ ├── _theme.scss │ │ ├── _variables.scss │ │ ├── app.scss │ │ └── dashboard.scss └── utils.js ├── package.json ├── public ├── scripts │ └── .gitignore └── styles │ └── .gitignore ├── server.js ├── tests └── index.js ├── webpack.config.base.js ├── webpack.config.development.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Please replace this line with full information about your idea or problem. If it's a bug share as much as possible to reproduce it 4 | 5 | --- 6 | 7 | Please preserve this line to notify @roll (lead of this repository) 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Please replace this line with full information about your pull request. Make sure that tests pass before publishing it 4 | 5 | --- 6 | 7 | Please preserve this line to notify @roll (lead of this repository) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Python 2 | .DS_Store 3 | .idea 4 | .projectile 5 | *.sublime-project 6 | *.sublime-workspace 7 | __pycache__/ 8 | *.py[cod] 9 | bower_components/* 10 | node_modules/* 11 | .publish/ 12 | .sass-cache 13 | _site/* 14 | .vscode/* 15 | jsconfig.json 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } 4 | -------------------------------------------------------------------------------- /LEAD.md: -------------------------------------------------------------------------------- 1 | roll 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Open Knowledge Foundation 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: templates 2 | 3 | 4 | LEAD := $(shell head -n 1 LEAD.md) 5 | 6 | 7 | all: list 8 | 9 | templates: 10 | sed -i -E "s/@(\w*)/@$(LEAD)/" .github/issue_template.md 11 | sed -i -E "s/@(\w*)/@$(LEAD)/" .github/pull_request_template.md 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Quality Dashboard 2 | 3 | Data Quality Dashboard provides access to, and displays statistics on, a collection of published data. This collection of data is logically related: for example, data published by a single government department, or a group of departments. 4 | 5 | The Data Quality Dashboard has been developed in order to display data quality information on the 25K spend data published by the UK Government on [data.gov.uk](https://data.gov.uk/). You can see and interact with this [instance of Data Quality Dashboard here](http://uk-25k.openspending.org/). It is powered by a static database generated with [Data Quality CLI](https://github.com/frictionlessdata/data-quality-cli) that you can find [here](https://github.com/okfn/data-quality-uk-25k-spend). 6 | 7 | The Dashboard can be used for any published collection of data by following a few key steps. 8 | 9 | ## Local development 10 | 11 | ``` 12 | # Get the code 13 | git clone https://github.com/okfn/data-quality-dashboard.git 14 | 15 | # Install the dependencies 16 | npm install 17 | 18 | # Just build the sources 19 | npm run build 20 | 21 | # Just run the server 22 | npm run start 23 | 24 | # View the app in your browser 25 | open http://localhost:3000/ 26 | ``` 27 | 28 | See the `scripts` section in `package.json` for more available commands. 29 | 30 | Read on for details. 31 | 32 | ## Application 33 | 34 | The Data Quality Dashboard is a Node.js application written in ES6, largely using Express and React. 35 | 36 | The `app.backend` module renders the basic views (using React on the server) and is responsible for preparing the data as JSON by parsing the CSV database. It also provides some simple routes for standard pages like FAQ and About. 37 | 38 | The `app.ui` module is a React-Redux application for displaying the data to the user. 39 | 40 | The codebase is written in Node.js-style CommonJS, using ES6 syntax. The `app.ui` code is bundled by (Webpack)[http://webpack.github.io/], and `app.backend` is transformed using Babel at runtime. 41 | 42 | ### Remote deployment 43 | 44 | We push to Heroku, and a `postinstall` script ensures that `app.ui` is bundled before the app is served. Make sure you set `NPM_CONFIG_PRODUCTION=false` to include `devDependencies` on Heroku. 45 | 46 | ## Data 47 | 48 | The Data Quality Dashboard reads data from a flat file storage, with data written to CSV and JSON. Any publicly available file storage will do, as long as the file naming and data structure of the files is consistent. 49 | 50 | Currently, we run the database for the UK Spend Publishing Dashboard from a public repository on GitHub. This gives easy access to the files, and enables a version history of the database. 51 | 52 | As GitHub does not support CORS, we then use a proxy that does - [RawGit](https://rawgit.com/). 53 | 54 | When the application loads, it reads the data from the database, parses the content to JSON, and stores the new data representation as JSON. This JSON representation is accessible via an API endpoint that the frontend app uses. 55 | 56 | To configure the database, the application needs to know the base path as a URL. 57 | 58 | For example: 59 | 60 | * `https://rawgit.com/okfn/data-quality-uk-25k-spend/master/data` 61 | 62 | By default, the application expects to find at that base the following files: 63 | 64 | * `instance.json`: Basic metadata for the instance 65 | * `sources.csv`: The list of data sources that are assessed for quality 66 | * `publishers.csv`: The list of publishers that produce these datasources 67 | * `results.csv`: The results as found by SPD-Admin 68 | * `performance.csv`: The performance as found by SPD-Admin 69 | * `runs.csv`: A log of the results run against these resources 70 | 71 | Of course, each of these files must conform to a certain datastructure - think of them as tables in a database. As long as you conform to the structure and expected data within that structure, it does not matter how the database is actually produced. 72 | 73 | For how to change the database see the [Configure database](#configure-database) section. 74 | 75 | ## Schema 76 | 77 | The Data Quality Dashboard expects the following schema. 78 | 79 | ### instance.json 80 | 81 | A single object with the following fields: 82 | 83 | * `name`: The name of this dashboard 84 | * `admin`: The email address of the administrator of this dashboard 85 | * `validator_url`: The URL to a GoodTables API endpoint (eg: `https://goodtables.okfnlabs.org/api/run`) 86 | * `last_modified`: Time when the data was last modified. Should be updated before each database deploy. 87 | 88 | ### sources.csv 89 | 90 | A CSV with the following columns: 91 | 92 | * `id`: A unique identifier for this data source. 93 | * `publisher_id`: The unique identifier of the publisher this data source belongs to. 94 | * `title`: A title for this data source. 95 | * `data`: The permalink URL for this data source. 96 | * `format`: The file format for this data source. 97 | * `last_modified`: The timestamp that indicates when this data source was last modified. 98 | * `period_id`: The publication period of the data source. 99 | * `schema`: The permalink URL for the schema that this data source should be validated against (if any). 100 | 101 | ### publishers.csv 102 | 103 | A CSV with the following columns: 104 | 105 | * `id`: A unique identifier for this publisher. 106 | * `title`: A proper title for this publisher. 107 | * `type`: A signifying type for this publisher. 108 | * `homepage`: The homepage of this publisher as a URL. 109 | * `contact`: The contact person for this publisher. 110 | * `email`: The contact email for this publisher. 111 | * `parent_id`: The parent publisher for this publisher (nested publishers). 112 | 113 | ### results.csv 114 | 115 | A CSV with the following columns: 116 | 117 | * `id`: A unique identifier for this result. 118 | * `source_id`: The identifier for the data source in this result. 119 | * `publisher_id`: The identifier for the publisher in this result. 120 | * `period_id`: The publication period of this result's data source. 121 | * `score`: The score for this result. 122 | * `data`: The permalink URL for this result's data source. 123 | * `schema`: The permalink URL for this result's data source schema (if any). 124 | * `summary`: A summary of this result. 125 | * `run_id`: The identifier of the run in which this result was generated. 126 | * `timestamp`: The timestamp for this result. 127 | * `report`: The base URL to a more detailed report 128 | 129 | ### performance.csv 130 | 131 | A CSV with the following columns: 132 | 133 | * `publisher_id`: The identifier for the publisher. 134 | * `period_id`: The time span for the analysis. 135 | * `files_count`: Number of files published during the above mentioned time span. 136 | * `score`: Score for the above mentioned files. 137 | * `valid`: How many of the above mentioned files are valid. 138 | * `files_count_to_date`: Total number of files published up to this period. 139 | * `score_to_date`: Score of all the files published up to this period. 140 | * `valid_to_date`: Number of valid files from all published up to this period. 141 | 142 | ### runs.csv 143 | 144 | A CSV with the following columns: 145 | 146 | * `id` 147 | * `timestamp` 148 | * `total_score` 149 | 150 | 151 | ## Configure database 152 | 153 | The database can be configured through the following environment variables: 154 | 155 | * `DATABASE_LOCATION`: Base URL for the files. 156 | * `PUBLISHER_TABLE`: Name of the file containing the publishers (relative to the DATABASE_LOCATION). 157 | 158 | Following this pattern, you can also configure `SOURCE_TABLE`, `RUN_TABLE`, `PERFORMANCE_TABLE` and `INSTANCE_TABLE`. 159 | 160 | ## Tooling 161 | 162 | In order to generate the result set for a Data Quality Dashboard, we build a command line utility that is designed to be run by a developer at regular intervals (as relevant for the data being assessed). This tool, [Data Quality CLI](https://github.com/okfn/data-quality-cli) is configurable to use in assessing data quality based on metrics of: 163 | 164 | * Timeliness 165 | * Structural Validity 166 | * Schema Validity 167 | 168 | Note that, like the Data Quality Dashboard itself, the CLI has currently only been tested on the UK 25K spend data. 169 | -------------------------------------------------------------------------------- /app/backend/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Promise from 'bluebird' 4 | import path from 'path' 5 | import fs from 'fs' 6 | 7 | Promise.promisifyAll(fs) 8 | 9 | function makePage(filename, title) { 10 | return function(req, res) { 11 | var filepath = path.join(req.app.get('config').get('contentDir'), filename) 12 | var backend_config = req.app.get('config').get('backend') 13 | fs.readFileAsync(filepath, 'utf8') 14 | .then(function(content) { 15 | return res.render('page', { 16 | content: content, 17 | title: title, 18 | showPricing: backend_config['showPricing'] 19 | }) 20 | }) 21 | .catch(console.trace.bind(console)) 22 | } 23 | } 24 | 25 | function dashboard(req, res) { 26 | var backend_config = req.app.get('config').get('backend') 27 | return res.render('dashboard', {embed: false, showPricing: backend_config['showPricing']}) 28 | } 29 | 30 | function embed(req, res) { 31 | return res.render('dashboard', {embed: true}) 32 | } 33 | 34 | function api(req, res) { 35 | var db = req.app.get('cache').get('db') 36 | return res.json(db) 37 | } 38 | 39 | function pricing(req, res){ 40 | var backend_config = req.app.get('config').get('backend') 41 | res.redirect(backend_config['pricingPageUrl']) 42 | } 43 | 44 | export default { 45 | about: makePage('about.md', 'About'), 46 | faq: makePage('faq.md', 'FAQ'), 47 | pricing, 48 | dashboard, 49 | embed, 50 | api 51 | } 52 | -------------------------------------------------------------------------------- /app/backend/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import controllers from './controllers' 4 | import middlewares from './middlewares' 5 | import routes from './routes' 6 | import services from './services' 7 | 8 | export { controllers, middlewares, routes, services } 9 | -------------------------------------------------------------------------------- /app/backend/middlewares.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import _ from 'lodash' 4 | import services from './services' 5 | 6 | function getInstance(req, res, next) { 7 | var cacheData = req.app.get('config').get('cacheData') 8 | services.getInstance() 9 | .then(function(result) { 10 | if (_.isEmpty(req.app.get('cache').get('instance')) || !cacheData) { 11 | // cache the instance 12 | req.app.get('cache').set('instance', result) 13 | return next() 14 | } 15 | if (req.app.get('cache').get('instance').last_modified != result.last_modified) { 16 | // flush cached data 17 | req.app.get('cache').flushAll(); 18 | req.app.get('cache').set('instance', result) 19 | return next() 20 | } 21 | else{ 22 | return next() 23 | } 24 | }) 25 | .catch(console.trace.bind(console)) 26 | } 27 | 28 | function getDB(req, res, next) { 29 | var cacheData = req.app.get('config').get('cacheData') 30 | if (_.isEmpty(req.app.get('cache').get('db')) || !cacheData) { 31 | services.makeDB() 32 | .then(function(result) { 33 | // cache the db 34 | req.app.get('cache').set('db', result) 35 | return next() 36 | }) 37 | .catch(console.trace.bind(console)) 38 | } else { 39 | return next() 40 | } 41 | } 42 | 43 | 44 | function setLocals(req, res, next) { 45 | res.locals.instance = req.app.get('cache').get('instance') || {} 46 | return next() 47 | } 48 | 49 | export default { getInstance, getDB, setLocals } 50 | -------------------------------------------------------------------------------- /app/backend/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Router } from 'express' 4 | import controllers from './controllers' 5 | 6 | let router = Router() 7 | 8 | export default function routes() { 9 | router.get('/about', controllers.about) 10 | router.get('/faq', controllers.faq) 11 | router.get('/pricing', controllers.pricing) 12 | router.get('/api', controllers.api) 13 | router.get('/embed', controllers.embed) 14 | router.get(/^(\/embed)\/.*/, controllers.embed) 15 | router.get('*', controllers.dashboard) 16 | return router 17 | } 18 | -------------------------------------------------------------------------------- /app/backend/services.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Promise from 'bluebird' 4 | import _ from 'lodash' 5 | import config from '../config' 6 | import utils from '../utils' 7 | 8 | function getInstance() { 9 | return utils.getJSONEndpoint(config.get('backend').instance) 10 | } 11 | 12 | function getPublisherData() { 13 | return utils.getCSVEndpoint(config.get('backend').publishers) 14 | } 15 | 16 | function getSourceData() { 17 | return utils.getCSVEndpoint(config.get('backend').sources) 18 | } 19 | 20 | function getResultData() { 21 | return utils.getCSVEndpoint(config.get('backend').results) 22 | } 23 | 24 | function getRunData() { 25 | return utils.getCSVEndpoint(config.get('backend').runs) 26 | } 27 | 28 | function getPerformanceData() { 29 | return utils.getCSVEndpoint(config.get('backend').performance) 30 | } 31 | 32 | function makeDB() { 33 | return Promise.join(getInstance(), getPublisherData(), getSourceData(), getResultData(), 34 | getRunData(), getPerformanceData(), processData) 35 | } 36 | 37 | function processData(instance, publishers, sources, results, runs, performance) { 38 | return { 39 | data: { 40 | instance: instance, 41 | publishers: publishers, 42 | sources: sources, 43 | results: results, 44 | runs: runs, 45 | performance: performance 46 | } 47 | } 48 | } 49 | 50 | export default { getInstance, makeDB } 51 | -------------------------------------------------------------------------------- /app/backend/views/dashboard.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import DefaultView from './default' 5 | 6 | class DashboardView extends Component { 7 | render() { 8 | const { instance, embed, showPricing} = this.props 9 | return ( 10 | 11 |
12 |
13 | ) 14 | } 15 | } 16 | 17 | export default DashboardView 18 | -------------------------------------------------------------------------------- /app/backend/views/default.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | 5 | class DefaultView extends Component { 6 | render() { 7 | const { children, instance, embed, showPricing } = this.props 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {instance.name} | {instance.organization} 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
Open Knowledge
26 | 54 |
55 | 56 | {children} 57 | 58 | 99 | 100 | 101 | 102 | 103 | ) 104 | } 105 | } 106 | 107 | export default DefaultView 108 | -------------------------------------------------------------------------------- /app/backend/views/page.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import marked from 'marked' 5 | import DefaultView from './default' 6 | 7 | class PageView extends Component { 8 | safe(content) { 9 | return { __html: marked(content) } 10 | } 11 | render() { 12 | const { instance, title, content, showPricing } = this.props 13 | return ( 14 | 15 |
16 |
17 |

{title}

18 |
19 |
20 |
21 |
22 |
23 | 24 | ) 25 | } 26 | } 27 | 28 | export default PageView 29 | -------------------------------------------------------------------------------- /app/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { createEngine } from 'express-react-views' 4 | import path from 'path' 5 | import config from './config' 6 | import _ from 'lodash' 7 | import NodeCache from 'node-cache' 8 | import { middlewares, routes } from './backend' 9 | 10 | export default function bootstrap(app, express) { 11 | let viewPath = path.join(__dirname, 'backend', 'views') 12 | let publicPath = path.join(path.dirname(__dirname), 'public') 13 | // NOTE: We compile ES6 at runtime, in server.js, hence transformViews is 14 | // false due to some weirdness in express-react-views 15 | // https://github.com/reactjs/express-react-views/issues/40 16 | let viewEngine = createEngine({transformViews: false}) 17 | let backendCache = new NodeCache() 18 | 19 | app.set('config', config) 20 | app.set('port', config.get('port')) 21 | app.set('cache', backendCache) 22 | app.set('views', viewPath) 23 | app.set('view engine', 'jsx') 24 | app.engine('jsx', viewEngine) 25 | app.use(express.static(publicPath)) 26 | app.use([ 27 | middlewares.getInstance, 28 | middlewares.getDB, 29 | middlewares.setLocals 30 | ]) 31 | app.use('', routes()) 32 | return app 33 | } 34 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import _ from 'lodash' 4 | import path from 'path' 5 | import nconf from 'nconf' 6 | import utils from './utils' 7 | 8 | nconf.file({ 9 | file: path.join(path.dirname(__dirname), 'settings.json') 10 | }) 11 | 12 | nconf.defaults({ 13 | port: process.env.PORT || 3000, 14 | backend: utils.getBackend(), 15 | cacheData: process.env.CACHE_DATA || true, 16 | contentDir: process.env.CONTENT_DIR || path.join(__dirname, 'content') 17 | }) 18 | 19 | export default nconf 20 | -------------------------------------------------------------------------------- /app/content/about.md: -------------------------------------------------------------------------------- 1 | The Spend Publishing Dashboard tracks the timeliness and quality of 2 | the spend data published by UK Government Departments as part of their 3 | transparency commitments. 4 | 5 | Based on the analysis of several thousand individual data files 6 | published by more than thirty departments, it provides a simple and 7 | easy-to-understand overview of performance on key metrics such as: 8 | 9 | * Timeliness: are departments publishing data (at all) and doing so in 10 | a timely manner? 11 | * Quality: are departments publishing "good" data, that is, well 12 | structured and in the standard, prescribed format? 13 | 14 | The goal of the dashboard is to support (and drive) improvement in the 15 | quality of expenditure data published by government entities---be it 16 | local authorities, departments or others. Specifically, it aims to: 17 | 18 | * Enable policy-makers and public able to see how departments and 19 | local authorities are performing against mandated publication 20 | requirements 21 | * Allow identification of best-practice (and worst-practice) for 22 | learning and improvement 23 | * Provide a starting point for those looking to acquire and 24 | consolidate data for their own work and analysis (for example, we 25 | have a list of the nearly 12,000 spend-related files published by UK 26 | government departments) 27 | 28 | As part of the development of the dashboard we have also created 29 | various related tools including the online service 30 | ["GoodTables"][goodtables] that allows users to check the quality of 31 | their CSV or XLS spend data files by validating them against existing 32 | government recommendations such as HMT recommendations for Departments 33 | and the 34 | [Local Government Transparency Code](https://www.gov.uk/government/publications/local-government-transparency-code-2014). 35 | 36 | [goodtables]: http://goodtables.okfnlabs.org/ 37 | 38 | ## Background 39 | 40 | The UK leads the world in terms of the publication of [open data][od] 41 | on public finances. Fiscal transparency and provision of open data in 42 | this area are seen as central to the government's transparency and 43 | open data strategy, helping to promote government efficiency and 44 | effectiveness and empowering citizens with an understanding of where 45 | their tax money goes. 46 | 47 | Data is published on spending at both the national and the local level 48 | along with related budgetary and financial data. Specifically, the 49 | Government requires regular publication of detailed, transactional, 50 | expenditure information by departments and local authorities - that 51 | is, information on all individual spending items from monthly mobile 52 | phones contracts to major software systems. 53 | 54 | At the national level, information on expenditure over £25k is one of 55 | the few mandated datasets that Departments must publish. Similarly at 56 | the local level, the 57 | [Local Government Transparency Code][transparency-code] requires 58 | publication of spending over £500 on a quarterly basis. Specifically 59 | paragraph 19, requires publication of itemised spending over £500 on a 60 | quarterly basis on items such as individual invoices, grant payments, 61 | expense payments, payments for goods and services, rent, credit notes 62 | over £500, and transactions with other public bodies. Paragraph 42 63 | recommends - but does not mandate - extending this to publishing on a 64 | monthly basis covering all items over £250 and including the total 65 | amount spent on remuneration over the period, as well as classifying 66 | expenditure using the Chartered Institute of Public Finance and 67 | Accountancy Service Reporting Code of Practice to enable comparability 68 | between local authorities. 69 | 70 | [transparency-code]: https://www.gov.uk/government/publications/local-government-transparency-code-2014 71 | 72 | [od]: http://okfn.org/open 73 | 74 | ## The Problem 75 | 76 | However, whilst the volume of data being is impressive, the quality is 77 | often less so. Poor quality data greatly reduces the usability and 78 | value of the data released - for business, for researchers, for 79 | journalists, for citizens and for government itself. Specific quality 80 | issues include: 81 | 82 | * Format: Data is frequently not provided in the recommended format. 83 | Even within a given department, data is often published in a variety 84 | of formats and structures spread over many files. For example, the 85 | Greater London Authority publishes their spend data in over 65 86 | different CSV files which between them are formatted in nearly 30 87 | different ways! This means that any user of the data must spend, 88 | literally, several days cleaning this data up in order to have a 89 | single consolidated set of data. 90 | * Timeliness: expenditure data is often not published by departments 91 | or local authorities on a timely basis. For example, the 92 | [UK Departmental Spending Report on data.gov.uk](https://data.gov.uk/data/openspending-report/index) 93 | shows that less than 15% of departments have published up-to-date 94 | spending data and some departments are more than 6 months out of 95 | date. This obviously substantially reduces the value of the data to 96 | many potential users both inside and outside government. 97 | * Missing “codings”: most interesting uses of spending data involving 98 | aggregating individual transactions by particular attribute - for 99 | example, calculating how much was spent with a given supplier 100 | involves summing all transactions with a particular supplier or 101 | calculating spend on training would involve summing all transactions 102 | coded as being related to training. However, many published datasets 103 | lack reliable codings. In particular, most spend data does not 104 | identify suppliers with a unique identifier such as a company number 105 | even within a single data file (let alone across data files from 106 | different publishers). Similarly, most spend data does not include 107 | any useful classification of transactions such as a code from a 108 | chart of accounts or a project code (such as included in HMT PESA 109 | data). This means data users must engage in the laborious (and 110 | error-prone) task of normalising and enhancing data (for example, 111 | attempting to correct variant spellings of the same company name or 112 | adding classifiers to expenditure). 113 | * Unconsolidated: data is published in individual files on a monthly 114 | or quarterly basis per department or local authority. However, most 115 | uses of the data involve access to more consolidated data (for 116 | example, one wants to see spending over a period of time (rather 117 | than for just one month) or to compare across departments or 118 | authorities). 119 | 120 | Lastly, though not a data quality issue in the narrow sense, we would add: 121 | 122 | * Usability: many (potential) users of spending data will struggle 123 | when presented with a simple page containing dozens of CSV 124 | files. Providing a simple browser and/or visualisations of the spend 125 | data is not hard to do and would greatly enhance the usability of 126 | the spending data to a large set of actual or potential users 127 | including policy-makers and citizens (whilst also encouraging people 128 | who wanted to dig deeper into the raw data to do so). Note: 129 | provision of these kind of interfaces is often directly dependent on 130 | resolving the previously mentioned quality issues (e.g. you can’t 131 | provide a useful visualisation without consolidated data that has 132 | useful codings). 133 | 134 | ## The Dashboard 135 | 136 | We need to drive and support improvements in the quality and usability 137 | of spending data. This Spend Publishing Dashboard is 138 | 139 | In particular, the Spend Publ exists to provide a simple and 140 | easy-to-understand overview of how 141 | 142 | Like all good simple visual representations it is based on large 143 | amounts of behind the scenes work. 144 | 145 | ## FAQs 146 | 147 | ### Is the project free/open-source? 148 | 149 | Yes, all code is open-source and is published on 150 | [GitHub](https://github.com/okfn/spend-publishing-dashboard). 151 | 152 | ### Is the data open? 153 | 154 | Yes, all the data we have produced---including a database of all 155 | spending files and their quality---is open and published online. 156 | 157 | ### Why are local authorities not included? 158 | 159 | We intend to also support spend data publication by local authorities. 160 | However, unlike departmental spending which is centralized on 161 | data.gov.uk and easily locatable due to consistent tagging, local 162 | authority spending is spread across hundreds of local authority 163 | websites in the UK. Tracking down the thousands of different data 164 | files ultimately has proved too resource-intensive for our limited 165 | funding. However, it is something we are focused on for the future. 166 | -------------------------------------------------------------------------------- /app/content/faq.md: -------------------------------------------------------------------------------- 1 | ## What is a "schema"? 2 | 3 | A schema is a description of what a given data structure (e.g. a 4 | single CSV file) should look like. Any deviation between the schema 5 | and the actual data structure could mean that manual work must be done 6 | to "correct" the data structure. 7 | 8 | ## How does the scoring system work? 9 | 10 | ## What does "correct" mean? 11 | 12 | "Correct" means 100% compliant with the specified schema. 13 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import express from 'express' 4 | import bootstrap from './bootstrap' 5 | 6 | export function start() { 7 | let app = express() 8 | app = bootstrap(app, express) 9 | app.listen(app.get('port'), function() { 10 | console.log('Serving from port ' + app.get('port')) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /app/ui/scripts/actions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import 'isomorphic-fetch' 4 | import _ from 'lodash' 5 | 6 | export const REQUEST_DATA = 'REQUEST_DATA' 7 | export const RECEIVE_DATA = 'RECEIVE_DATA' 8 | export const REQUEST_ACTIVE_PUBLISHER = 'REQUEST_ACTIVE_PUBLISHER' 9 | export const RECEIVE_ACTIVE_PUBLISHER = 'RECEIVE_ACTIVE_PUBLISHER' 10 | 11 | function requestData() { 12 | return { 13 | type: REQUEST_DATA, 14 | payload: null 15 | } 16 | } 17 | 18 | function receiveData(json) { 19 | return { 20 | type: RECEIVE_DATA, 21 | payload: json.data 22 | } 23 | } 24 | 25 | function requestActivePublisher() { 26 | return { 27 | type: REQUEST_ACTIVE_PUBLISHER, 28 | payload: null 29 | } 30 | } 31 | 32 | function receiveActivePublisher(data) { 33 | return { 34 | type: RECEIVE_ACTIVE_PUBLISHER, 35 | payload: data 36 | } 37 | } 38 | 39 | function fetchData(lookup) { 40 | return dispatch => { 41 | dispatch(requestData()) 42 | let promises = fetch('/api') 43 | .then(response => response.json()) 44 | .then(json => dispatch(receiveData(json))) 45 | if (lookup) { 46 | promises = promises 47 | .then(data => dispatch(getActivePublisherIfNeeded(lookup))) 48 | } 49 | return promises 50 | } 51 | } 52 | 53 | function getActivePublisher(data, lookup) { 54 | return dispatch => { 55 | dispatch(requestActivePublisher()) 56 | let activePublisher = _.find(data.publishers, { 'id': lookup }) 57 | activePublisher.sources = _.filter(data.sources, { 'publisher_id': lookup }) 58 | activePublisher.results = _.filter(data.results, { 'publisher_id': lookup }) 59 | activePublisher.performance = _.filter(data.performance, 60 | { 'publisher_id': lookup }) 61 | let newData = Object.assign({}, data, { activePublisher: activePublisher }) 62 | return dispatch(receiveActivePublisher(newData)) 63 | } 64 | } 65 | 66 | function shouldFetchData(state) { 67 | const { ui, data } = state 68 | if (data.isEmpty) { 69 | return true 70 | } 71 | if (ui.isFetching) { 72 | return false 73 | } 74 | return true 75 | } 76 | 77 | function shouldGetActivePublisher(data, lookup) { 78 | if (_.isEmpty(data.activePublisher)) { 79 | return true 80 | } 81 | if (data.activePublisher.id === lookup) { 82 | return false 83 | } 84 | return true 85 | } 86 | 87 | export function getActivePublisherIfNeeded(lookup) { 88 | return (dispatch, getState) => { 89 | const { data } = getState() 90 | if (shouldGetActivePublisher(data, lookup)) { 91 | return dispatch(getActivePublisher(data, lookup)) 92 | } 93 | } 94 | } 95 | 96 | export function fetchDataIfNeeded(lookup) { 97 | return (dispatch, getState) => { 98 | const currentState = getState() 99 | let needData, needActivePublisher 100 | if (shouldFetchData(currentState)) { 101 | needData = true 102 | } 103 | if (shouldGetActivePublisher(currentState, lookup)) { 104 | needActivePublisher = true 105 | } 106 | if (needData && !needActivePublisher) { 107 | return dispatch(fetchData()) 108 | } 109 | if (needActivePublisher && !needData) { 110 | return dispatch(getActivePublisher(currentState.data, lookup)) 111 | } 112 | if (needData && needActivePublisher) { 113 | return dispatch(fetchData(lookup)) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/ui/scripts/components/charts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Main from './main' 4 | import Publisher from './publisher' 5 | 6 | export { Main, Publisher } 7 | -------------------------------------------------------------------------------- /app/ui/scripts/components/charts/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { Line as LineChart } from 'react-chartjs' 5 | import { ui as UIUtils } from '../../utils' 6 | import { calc as CalcUtils } from '../../utils' 7 | 8 | class MainChart extends Component { 9 | render() { 10 | let linePayload = UIUtils.makeScoreLinePayload( 11 | this.props.results, 12 | this.props.performance 13 | ) 14 | return ( 15 |
16 |
{UIUtils.makeLegend()}
17 | 24 |
25 | ) 26 | } 27 | } 28 | 29 | export default MainChart 30 | -------------------------------------------------------------------------------- /app/ui/scripts/components/charts/publisher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { Line as LineChart } from 'react-chartjs' 5 | import { ui as UIUtils } from '../../utils' 6 | import { calc as CalcUtils } from '../../utils' 7 | 8 | class PublisherChart extends Component { 9 | render() { 10 | var linePayload = UIUtils.makeScoreLinePayload( 11 | this.props.results, 12 | this.props.performance 13 | ) 14 | return ( 15 |
16 |
{UIUtils.makeLegend()}
17 | 24 |
25 | ) 26 | } 27 | } 28 | 29 | export default PublisherChart 30 | -------------------------------------------------------------------------------- /app/ui/scripts/components/charts/source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { Line as LineChart } from 'react-chartjs' 5 | import { ui as UIUtils } from '../../utils' 6 | import { calc as CalcUtils } from '../../utils' 7 | 8 | class SourceChart extends Component { 9 | render() { 10 | var linePayload = UIUtils.makeScoreLinePayload(this.props.results) 11 | return ( 12 |
13 |
14 |
15 |

{this.props.source.title} ({this.props.publisher.title})

16 |

17 |
18 |
19 | More 20 |
21 |
22 | 29 |
30 | ) 31 | } 32 | } 33 | 34 | export default SourceChart 35 | -------------------------------------------------------------------------------- /app/ui/scripts/components/overviews/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Main from './main' 4 | import Publisher from './publisher' 5 | 6 | export { Main, Publisher } 7 | -------------------------------------------------------------------------------- /app/ui/scripts/components/overviews/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { ui as UIUtils } from '../../utils' 5 | 6 | class MainOverview extends Component { 7 | render() { 8 | const { instance, publishers, results } = this.props 9 | return ( 10 |
11 |

{instance.name}

12 |

13 | {instance.pitch} 14 |

15 |
    16 | {UIUtils.makeOverview(results, publishers, 'main')} 17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export default MainOverview 24 | -------------------------------------------------------------------------------- /app/ui/scripts/components/overviews/publisher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { ui as UIUtils } from '../../utils' 5 | 6 | class PublisherOverview extends Component { 7 | render() { 8 | const { publisher, results } = this.props 9 | return ( 10 |
11 |

{publisher.title}

12 |
    13 | {UIUtils.makeOverview(results, [], 'publisher')} 14 |
15 |
16 | ) 17 | } 18 | } 19 | 20 | export default PublisherOverview 21 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { Input } from 'react-bootstrap' 5 | import { ui as UIUtils } from '../../utils' 6 | 7 | class TableFilter extends Component { 8 | render() { 9 | return ( 10 | 17 | ) 18 | } 19 | } 20 | 21 | export default TableFilter 22 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/head.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | var _ = require('lodash') 5 | var Popover = require('react-bootstrap').Popover 6 | var OverlayTrigger = require('react-bootstrap').OverlayTrigger 7 | 8 | class TableHead extends Component { 9 | onClick(e) { 10 | // this.props.sort(this.props.column.key) 11 | } 12 | render() { 13 | const { key, column, sort } = this.props 14 | let tooltip = '' 15 | if (column.help) { 16 | tooltip = ( 17 | {_.capitalize(column.help)}} 21 | > 22 | 23 | 24 | ) 25 | } 26 | return ( 27 | 28 | 29 | {_.capitalize(column.label || column.key)} 30 | {tooltip} 31 | 32 | ) 33 | } 34 | } 35 | 36 | export default TableHead 37 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Table from './table'; 4 | import Resize from './resize'; 5 | import Info from './info'; 6 | import Head from './head'; 7 | import Filter from './filter'; 8 | 9 | export { Table, Resize, Info, Head, Filter }; 10 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/info.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class TableInfo extends Component { 4 | render() { 5 | return ( 6 | 7 | Showing X of X results 8 | 9 | ) 10 | } 11 | } 12 | 13 | export default TableInfo 14 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/resize.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class TableResize extends Component { 4 | render() { 5 | return ( 6 | 7 | Show less | 8 | Show more 9 | 10 | ) 11 | } 12 | } 13 | 14 | export default TableResize 15 | -------------------------------------------------------------------------------- /app/ui/scripts/components/tables/table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import _ from 'lodash' 5 | import { ui as UIUtils } from '../../utils' 6 | import { Table } from 'react-bootstrap' 7 | import TableHead from './head' 8 | 9 | class TableComponent extends Component { 10 | render() { 11 | const {columns, rows, results, sort, title, parentRoute } = this.props 12 | const rowKeys = _.keys(_.castArray(rows)[0]) 13 | const existingColumns = _.filter(columns, function(obj) { 14 | return (obj.mandatory || _.indexOf(rowKeys, obj.key) !== -1) 15 | }) 16 | return ( 17 |
18 |
19 |
20 |

{_.capitalize(title)}

21 |
22 |
23 | 24 | 25 | 26 | {existingColumns.map(function(column) { 27 | return 29 | })} 30 | 31 | 32 | 33 | {UIUtils.makeTableBody(rows, results, 34 | {'route': title, 'sort': sort, 'columns': existingColumns, 'parentRoute': parentRoute})} 35 | 36 |
37 |
38 | ) 39 | } 40 | } 41 | 42 | export default TableComponent 43 | -------------------------------------------------------------------------------- /app/ui/scripts/containers/App.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component } from 'react' 4 | import { Provider } from 'react-redux' 5 | import { Route } from 'react-router' 6 | import { ReduxRouter } from 'redux-router' 7 | import configureStore from '../store/configureStore' 8 | import Main from './Main' 9 | import Publisher from './Publisher' 10 | import Embed from './Embed' 11 | 12 | const store = configureStore() 13 | 14 | class App extends Component { 15 | render() { 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ) 28 | } 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /app/ui/scripts/containers/Embed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component, PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | import { selectPublisher, fetchDataIfNeeded } from '../actions' 6 | import { Table } from '../components/tables' 7 | import { Main as Overview } from '../components/overviews' 8 | 9 | class Embed extends Component { 10 | componentDidMount() { 11 | const { dispatch } = this.props 12 | dispatch(fetchDataIfNeeded()) 13 | } 14 | render() { 15 | const { ui, data, route } = this.props 16 | return ( 17 |
18 | {ui.isFetching && 19 |
20 |
Loading data..
21 |
22 | } 23 | {!ui.isFetching && 24 |
25 |
26 |
27 | 29 |
30 |
31 |
32 | 35 | 36 | 37 | 38 | } 39 | 40 | ) 41 | } 42 | } 43 | 44 | Embed.propTypes = { 45 | data: PropTypes.object.isRequired, 46 | ui: PropTypes.object.isRequired, 47 | dispatch: PropTypes.func.isRequired 48 | } 49 | 50 | function mapStateToProps(state) { 51 | const { ui, data } = state 52 | return { ui, data } 53 | } 54 | 55 | export default connect(mapStateToProps)(Embed) 56 | -------------------------------------------------------------------------------- /app/ui/scripts/containers/Main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component, PropTypes } from 'react' 4 | import marked from 'marked' 5 | import { connect } from 'react-redux' 6 | import { selectPublisher, fetchDataIfNeeded } from '../actions' 7 | import { Table } from '../components/tables' 8 | import { Main as Overview } from '../components/overviews' 9 | 10 | class Main extends Component { 11 | componentDidMount() { 12 | const { dispatch } = this.props 13 | dispatch(fetchDataIfNeeded()) 14 | } 15 | safe(content) { 16 | return { __html: marked(content) } 17 | } 18 | render() { 19 | const { ui, data, route } = this.props 20 | return ( 21 |
22 | {ui.isFetching && 23 |
24 |
Loading data..
25 |
26 | } 27 | {!ui.isFetching && 28 |
29 |
30 |
31 | 33 |
34 |
35 |
38 | 39 | {data.instance.context && 40 |
41 |
42 |

What's this all about?

43 |
45 |
46 |
47 | } 48 | 49 | 50 | } 51 | 52 | ) 53 | } 54 | } 55 | 56 | Main.propTypes = { 57 | data: PropTypes.object.isRequired, 58 | ui: PropTypes.object.isRequired, 59 | dispatch: PropTypes.func.isRequired 60 | } 61 | 62 | function mapStateToProps(state) { 63 | const { ui, data } = state 64 | return { ui, data } 65 | } 66 | 67 | export default connect(mapStateToProps)(Main) 68 | -------------------------------------------------------------------------------- /app/ui/scripts/containers/Publisher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Component, PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | import { getActivePublisherIfNeeded, fetchDataIfNeeded } from '../actions' 6 | import { Table } from '../components/tables' 7 | import { Publisher as Overview } from '../components/overviews' 8 | import { Publisher as Chart } from '../components/charts' 9 | 10 | class Publisher extends Component { 11 | componentDidMount() { 12 | const { dispatch } = this.props 13 | const lookup = this.props.params.lookup 14 | dispatch(fetchDataIfNeeded(lookup)) 15 | } 16 | render() { 17 | const { ui, data, route } = this.props 18 | const activePublisher = data.activePublisher 19 | return ( 20 |
21 | {ui.isFetching && 22 |
23 |
Loading data..
24 |
25 | } 26 | {!ui.isFetching && 27 |
28 |
29 |
30 | 32 |
33 |
34 | 36 |
37 |
38 |
42 | 43 | 44 | 45 | } 46 | 47 | ) 48 | } 49 | } 50 | 51 | Publisher.propTypes = { 52 | data: PropTypes.object.isRequired, 53 | ui: PropTypes.object.isRequired, 54 | dispatch: PropTypes.func.isRequired 55 | } 56 | 57 | function mapStateToProps(state) { 58 | const { ui, data } = state 59 | return { ui, data } 60 | } 61 | 62 | export default connect(mapStateToProps)(Publisher) 63 | -------------------------------------------------------------------------------- /app/ui/scripts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // for webpack to build our css 4 | require('../styles/app.scss') 5 | 6 | import 'babel-polyfill' 7 | import React from 'react' 8 | import Chart from 'chart.js' 9 | import { render } from 'react-dom' 10 | import App from './containers/App' 11 | 12 | render( 13 | , 14 | document.getElementById('application') 15 | ) 16 | -------------------------------------------------------------------------------- /app/ui/scripts/reducers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { combineReducers } from 'redux' 4 | import { routerStateReducer } from 'redux-router' 5 | import { data as dataUtils } from '../utils' 6 | import { 7 | REQUEST_DATA, 8 | RECEIVE_DATA, 9 | RECEIVE_ACTIVE_PUBLISHER, 10 | REQUEST_ACTIVE_PUBLISHER 11 | } from '../actions' 12 | 13 | function ui(state = { 14 | isFetching: false, 15 | tableHeaders: { 16 | main: [ 17 | {key: 'title', mandatory: true}, 18 | {key: 'type', mandatory: false}, 19 | {key: 'homepage', mandatory: false}, 20 | {key: 'email', mandatory: false}, 21 | {key: 'completelyCorrect', label: 'Valid files', 22 | help: 'number of files with no errors at all', mandatory: true}, 23 | {key: 'score', label: 'All-time Score', mandatory: true}, 24 | {key: 'lastFileDate', label: 'last file', mandatory: true}, 25 | {key: 'lastFileScore', label: 'Current Score', mandatory: true, 26 | help: 'Average score (percent of correctness) for files published over the last three months'} 27 | ], 28 | publisher: [ 29 | {key:'period', label:'period', mandatory: true}, 30 | {key:'title', mandatory: true}, 31 | {key:'data', label:'URL', mandatory: true}, 32 | {key:'format', mandatory: true}, 33 | {key:'report', label:'Error details', mandatory: true}, 34 | {key:'score', mandatory: true}, 35 | {key:'schema', mandatory: false} 36 | ] 37 | }, 38 | tableSorters: { 39 | main: [ 40 | ['lastFileScore', 'desc'], 41 | ['title', 'asc'] 42 | ], 43 | publisher: [ 44 | ['period', 'desc'], 45 | ['score', 'desc'] 46 | ] 47 | } 48 | }, action) { 49 | switch (action.type) { 50 | case REQUEST_DATA: 51 | return Object.assign({}, state, { 52 | isFetching: true 53 | }) 54 | case REQUEST_ACTIVE_PUBLISHER: 55 | return Object.assign({}, state, { 56 | isFetching: true 57 | }) 58 | case RECEIVE_DATA: 59 | return Object.assign({}, state, { 60 | isFetching: false 61 | }) 62 | case RECEIVE_ACTIVE_PUBLISHER: 63 | return Object.assign({}, state, { 64 | isFetching: false 65 | }) 66 | default: 67 | return state 68 | } 69 | } 70 | 71 | function data(state = { 72 | isEmpty: true, 73 | instance: {}, 74 | publishers: [], 75 | sources: [], 76 | results: [], 77 | runs: [], 78 | performance: [], 79 | activePublisher: {} 80 | }, action) { 81 | switch (action.type) { 82 | case RECEIVE_DATA: 83 | return Object.assign({}, state, action.payload, { isEmpty: false }) 84 | case RECEIVE_ACTIVE_PUBLISHER: 85 | return Object.assign({}, state, action.payload, { isEmpty: false }) 86 | default: 87 | return state 88 | } 89 | } 90 | 91 | const rootReducer = combineReducers({ 92 | router: routerStateReducer, 93 | ui, 94 | data 95 | }) 96 | 97 | export default rootReducer 98 | -------------------------------------------------------------------------------- /app/ui/scripts/store/configureStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { compose, createStore, applyMiddleware } from 'redux' 4 | import thunkMiddleware from 'redux-thunk' 5 | import createLogger from 'redux-logger' 6 | import { reduxReactRouter } from 'redux-router' 7 | import { createHistory } from 'history' 8 | import rootReducer from '../reducers' 9 | 10 | const createStoreWithMiddleware = compose( 11 | applyMiddleware( 12 | thunkMiddleware, 13 | createLogger() 14 | ), 15 | reduxReactRouter({ createHistory }) 16 | )(createStore) 17 | 18 | export default function configureStore(initialState) { 19 | return createStoreWithMiddleware(rootReducer, initialState) 20 | } 21 | -------------------------------------------------------------------------------- /app/ui/scripts/utils/calc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import _ from 'lodash' 4 | import moment from 'moment' 5 | 6 | function publisherCount(publishers) { 7 | return _.uniqBy(publishers, 'id').length 8 | } 9 | 10 | function recentPeriodResults(results) { 11 | let today = new Date() 12 | let three_months_ago = new Date(today.getFullYear(), today.getMonth()-3) 13 | let recentPeriodOnly = [] 14 | 15 | if (results.length > 0) { 16 | recentPeriodOnly = _.filter(results, function(obj) { 17 | return moment(obj.created_at, 'YYYY-MM-DD', true).toDate() > three_months_ago 18 | }) 19 | } 20 | return recentPeriodOnly 21 | } 22 | 23 | function validPercent(results) { 24 | let validPercent = 0 25 | let valid = _.filter(results, function(obj) { 26 | let score = obj.score ? parseInt(obj.score) : 0 27 | if (score == 100) { 28 | return obj 29 | } 30 | }) 31 | if (results.length > 0) { 32 | validPercent = Math.round((valid.length / results.length) * 100) 33 | } 34 | return validPercent 35 | } 36 | 37 | function totalScore(results, numberOfPublishers, numberOfTimeUnits) { 38 | let scores = [] 39 | let score = 0 40 | let expectedResults = numberOfPublishers * numberOfTimeUnits 41 | _.forEach(results, function(obj) { 42 | scores.push(parseInt(obj.score)) 43 | }) 44 | if (scores.length > 0) { 45 | let sumScores = _.reduce(scores, function(sum, n) {return sum + n}) 46 | if (scores.length > expectedResults) { expectedResults = scores.length } 47 | score = Math.round(sumScores / expectedResults) 48 | } 49 | return score 50 | } 51 | 52 | function publisherScore(publisher, results) { 53 | let scores = [] 54 | let countCorrect = 0 55 | let publisherScore = 0 56 | 57 | // get all scores for this publisher from results 58 | _.forEach(results, function(obj) { 59 | if (obj.publisher_id === publisher) { 60 | let score = obj.score ? parseInt(obj.score) : 0 61 | scores.push(score) 62 | if (score === 100) { 63 | countCorrect += 1 64 | } 65 | } 66 | }) 67 | // set the publisher score to: sum of scores / number of scores 68 | if (scores.length > 0) { 69 | publisherScore = Math.round(_.reduce(scores, function(sum, n) {return sum + n}) / scores.length) 70 | } 71 | return {'score': publisherScore, 'amountCorrect': countCorrect} 72 | } 73 | 74 | // return last publication date for a give publisher 75 | function lastFile(publisher, results) { 76 | 77 | let publisherFiles = _.filter(results, function(obj) { 78 | return obj.publisher_id === publisher 79 | }) 80 | let publication = _.maxBy(publisherFiles, function(obj) { 81 | return moment(obj.created_at, 'YYYY-MM-DD', true).utc().toDate() 82 | }) 83 | 84 | let lastFile = {period: 0, score: 0} 85 | if (publication) { 86 | lastFile.period = moment(publication.created_at, 'YYYY-MM-DD', true).utc().toDate() 87 | lastFile.score = parseInt(publication.score) 88 | } 89 | return lastFile 90 | } 91 | 92 | // return the data for a source 93 | function sourceData(source, results) { 94 | let sourceData = {score: 0, timestamp: 0, publicationDate: 0} 95 | let matchingSource = _.find(results, _.matchesProperty('source_id', source)) 96 | if (_.isUndefined(matchingSource) === false) { 97 | sourceData.score = matchingSource.score ? parseInt(matchingSource.score) : 0 98 | sourceData.timestamp = Date.parse(matchingSource.timestamp) 99 | sourceData.publicationDate = moment(matchingSource.created_at, 'YYYY-MM-DD', true).utc().toDate() 100 | } 101 | return sourceData 102 | } 103 | 104 | // return the latest results for all sources 105 | function latestResults(results) { 106 | return _.chain(results) 107 | .orderBy(function(obj) {return Date.parse(obj.timestamp)}, ['desc']) 108 | .uniqBy(function (e) { return e.source_id }) 109 | .value() 110 | } 111 | 112 | export default { 113 | publisherCount, 114 | recentPeriodResults, 115 | validPercent, 116 | totalScore, 117 | publisherScore, 118 | sourceData, 119 | lastFile, 120 | latestResults 121 | } 122 | -------------------------------------------------------------------------------- /app/ui/scripts/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import calc from './calc' 4 | import ui from './ui' 5 | 6 | export { calc, ui } 7 | -------------------------------------------------------------------------------- /app/ui/scripts/utils/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import _ from 'lodash' 5 | import { Popover } from 'react-bootstrap' 6 | import { OverlayTrigger } from 'react-bootstrap' 7 | import { Link } from 'react-router' 8 | import CalcUtils from './calc' 9 | import moment from 'moment' 10 | 11 | function searchIn(objects, field, query) { 12 | let lookup = _.map(objects, field) 13 | let matches = _.filter(lookup, function(candidate) { 14 | return candidate.toLowerCase().indexOf(query.toLowerCase()) > -1 15 | }) 16 | return matches 17 | } 18 | 19 | function makeOverviewNumber(number, digitWidth) { 20 | let spans = [] 21 | let spanStyle = { 22 | width: Math.floor(digitWidth) + 'px', 23 | height: Math.floor(1.6 * digitWidth) + 'px', 24 | fontSize: Math.floor(1.425 * digitWidth) + 'px', 25 | lineHeight: Math.floor(1.55 * digitWidth) + 'px' 26 | } 27 | _.forEach(number, function(c) { 28 | spans.push({c}) 29 | }) 30 | return spans 31 | } 32 | 33 | function makeOverviewCounter(label, number, help, counterPadding, digitWidth) { 34 | let counterStyle 35 | let tooltip = '' 36 | if (counterPadding > 0) { 37 | let digitCount = number.length 38 | let counterWidth = (digitCount * (digitWidth + 6)) + (2 * counterPadding) 39 | counterStyle = { 40 | width: counterWidth + 'px', 41 | paddingLeft: counterPadding + 'px', 42 | paddingRight: counterPadding + 'px' 43 | } 44 | } else { 45 | counterStyle = { 46 | width: '100%', 47 | paddingLeft: '0', 48 | paddingRight: '0' 49 | } 50 | } 51 | if (help) { 52 | tooltip = ( 53 | {_.capitalize(help)}}> 54 | 55 | 56 | ) 57 | } 58 | return
  • 59 | {makeOverviewNumber(number, digitWidth)} 60 | {label} {tooltip}
  • 61 | } 62 | 63 | function makeOverview(results, objects, page) { 64 | let documentWidth = document.body.clientWidth 65 | let availableWidth = 0 66 | let counters = [] 67 | let digitMargins = 6 68 | let digitCount = 0 69 | let values, allDigitWidth, spacePerDigit, digitMaxWidth, digitWidth, 70 | counterPadding 71 | let latestResults = CalcUtils.latestResults(results) 72 | let recents = CalcUtils.recentPeriodResults(latestResults) 73 | if (page === 'main') { 74 | values = { 75 | validPercent: { 76 | label: 'valid (%)', 77 | help: 'percentage of valid files (no errors) published over the last three months', 78 | value: CalcUtils.validPercent(recents) + '' 79 | }, 80 | totalScore: { 81 | label: 'score (%)', 82 | help: 'average score (percent of correctness) for files published over the last three months', 83 | value: CalcUtils.totalScore(recents, CalcUtils.publisherCount(objects), 3) + '' 84 | }, 85 | publisherCount: { 86 | label: 'publishers', 87 | value: CalcUtils.publisherCount(objects) + '' 88 | }, 89 | sourceCount: { 90 | label: 'data files', 91 | value: latestResults.length + '' 92 | } 93 | } 94 | } else if (page === 'publisher') { 95 | values = { 96 | totalScore: { 97 | label: 'score (%)', 98 | help: 'average % correct (no errors) published over the last three months', 99 | value: CalcUtils.totalScore(recents, 1, 3) + '' 100 | }, 101 | validPercent: { 102 | label: 'correct (%)', 103 | help: 'percentage of valid files (rounded) published over the last three months', 104 | value: CalcUtils.validPercent(recents) + '' 105 | }, 106 | sourceCount: { 107 | label: 'data files', 108 | value: latestResults.length + '' 109 | } 110 | } 111 | } 112 | 113 | if (documentWidth >= 980 && documentWidth < 1180) { 114 | availableWidth = 980 115 | } else if (documentWidth >= 1180) { 116 | availableWidth = 1180 117 | } 118 | 119 | if (availableWidth > 0) { 120 | _.forEach(values, function(obj) { 121 | digitCount += obj.value.length 122 | }) 123 | spacePerDigit = availableWidth / (digitCount + 4) 124 | digitMaxWidth = spacePerDigit - digitMargins 125 | digitWidth = digitMaxWidth >= 80 ? 80 : digitMaxWidth 126 | allDigitWidth = digitCount * (digitWidth + digitMargins) 127 | counterPadding = (availableWidth - allDigitWidth) / 8 128 | _.forEach(values, function(obj) { 129 | counters.push(makeOverviewCounter(obj.label, obj.value, obj.help, counterPadding, digitWidth)) 130 | }) 131 | } else { 132 | _.forEach(values, function(obj) { 133 | digitCount = obj.value.length 134 | spacePerDigit = documentWidth / (digitCount + 2) 135 | digitMaxWidth = spacePerDigit - digitMargins 136 | digitWidth = digitMaxWidth >= 80 ? 80 : digitMaxWidth 137 | allDigitWidth = digitCount * (digitWidth + digitMargins) 138 | counterPadding = 0 139 | counters.push(makeOverviewCounter(obj.label, obj.value, obj.help, counterPadding, digitWidth)) 140 | }) 141 | } 142 | 143 | return counters 144 | } 145 | 146 | function makeTableBody(objects, results, options) { 147 | let _body = [] 148 | let _unsorted = [] 149 | let latestResults = CalcUtils.latestResults(results) 150 | 151 | if (options.route === 'publishers') { 152 | // for each publisher, get its score from results and return a new array of publishers with scores 153 | _unsorted = _.map(objects, function(obj) { 154 | let _publisherScore = CalcUtils.publisherScore(obj.id, latestResults) 155 | let _lastFile = CalcUtils.lastFile(obj.id, latestResults) 156 | let _objWithScore = _.cloneDeep(obj) 157 | _objWithScore.completelyCorrect = _publisherScore.amountCorrect 158 | _objWithScore.score = _publisherScore.score 159 | _objWithScore.lastFileDate = _lastFile.period 160 | _objWithScore.lastFileScore = _lastFile.score 161 | return _objWithScore 162 | }) 163 | } else if (options.route === 'data files') { 164 | // for each source, get its score and timestamp from results and return a new array of sources with scores and timestamps 165 | _unsorted = _.map(objects, function(obj) { 166 | let _sourceData = CalcUtils.sourceData(obj.id, latestResults) 167 | let _objWithScore = _.cloneDeep(obj) 168 | _objWithScore.score = _sourceData.score 169 | _objWithScore.timestamp = _sourceData.timestamp 170 | _objWithScore.period = _sourceData.publicationDate 171 | return _objWithScore 172 | }) 173 | } 174 | 175 | // sort 176 | let sorters = _.unzip(options.sort) 177 | _body = _.orderBy(_unsorted, sorters[0], sorters[1]) 178 | // for each data item, return a table row 179 | _body = _.map(_body, function(obj) { 180 | return
    {makeTableRow(obj, options, options.route)} 181 | }) 182 | return _body 183 | } 184 | 185 | function formatCell(key, value, obj, options) { 186 | let _cell 187 | let _c 188 | let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 189 | 'August', 'September', 'October', 'November', 'December'] 190 | 191 | switch (key) { 192 | case "title": 193 | if (options.route) { 194 | let path = (options.parentRoute == '/' ? '' : options.parentRoute) 195 | _cell = 196 | } else { 197 | _cell = 198 | } 199 | break 200 | case 'homepage': 201 | _cell = 202 | break 203 | case 'email': 204 | if (value) { 205 | _cell = 206 | } else { 207 | _cell = 208 | } 209 | break 210 | case 'score': 211 | case 'lastFileScore': 212 | if (value == 0) { 213 | _c = 'danger' 214 | } else if (value >= 1 && value <= 20) { 215 | _c = 'score-20p' 216 | } else if (value >= 21 && value <= 40) { 217 | _c = 'score-40p' 218 | } else if (value >= 41 && value <= 60) { 219 | _c = 'score-60p' 220 | } else if (value >= 61 && value <= 80) { 221 | _c = 'score-80p' 222 | } else if (value >= 81 && value <= 99) { 223 | _c = 'score-99p' 224 | } else { 225 | _c = 'success' 226 | } 227 | _cell = 228 | break 229 | case 'lastFileDate': 230 | let displayed_period = 'No publications' 231 | let today = new Date() 232 | let three_months_ago = new Date(today.getFullYear(), today.getMonth()-3) 233 | let one_year_ago = new Date(today.getFullYear()-1, today.getMonth()) 234 | _c = 'danger' 235 | if (value) { 236 | let date = new Date(value) 237 | let month = months[date.getMonth()] 238 | let year = date.getFullYear() 239 | if (date > three_months_ago) { 240 | _c = 'success' 241 | } else if (date > one_year_ago) { 242 | _c = 'warning' 243 | } 244 | displayed_period = month + ' ' + year 245 | } 246 | _cell = 247 | break 248 | case 'type': 249 | _cell = 250 | break 251 | case 'data': 252 | if (value) { 253 | let data_file_name = _.last(value.split('/')) 254 | _cell = 255 | } else { 256 | _cell = 257 | } 258 | break 259 | case 'period': 260 | if (value) { 261 | let month = months[value.getMonth()] 262 | let year = value.getFullYear() 263 | let displayed_period = month + ' ' + year 264 | _cell = 265 | } else { 266 | _cell = 267 | } 268 | break 269 | case 'report': 270 | if (!obj.schema) { obj.schema = ''} 271 | _cell = 272 | break 273 | case 'schema': 274 | if (value) { 275 | _cell = 276 | } else { 277 | _cell = 278 | } 279 | break 280 | default: 281 | _cell = 282 | } 283 | return _cell 284 | } 285 | 286 | function makeTableRow(obj, options, table) { 287 | let _row = [] 288 | let _cell 289 | if (table === 'publishers') { 290 | _.forEach(options.columns, function(column) { 291 | _cell = formatCell(column.key, obj[column.key], obj, options) 292 | if (_cell) { _row.push(_cell) } 293 | }) 294 | } else if (table === 'data files') { 295 | _.forEach(options.columns, function(column) { 296 | _cell = formatCell(column.key, obj[column.key], obj, {}) 297 | if (_cell) { _row.push(_cell) } 298 | }) 299 | } 300 | return _row 301 | } 302 | 303 | function makeLabel(timestamp) { 304 | let date = new Date(timestamp) 305 | let year = date.getFullYear() 306 | let month = date.getMonth() 307 | let abbr_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 308 | 'Sep', 'Oct', 'Nov', 'Dec'] 309 | return abbr_months[month] + ' ' + year 310 | } 311 | 312 | function makeChartData(performance) { 313 | let performances = [] 314 | let data = {} 315 | let scores = [] 316 | let valids = [] 317 | let labels = [] 318 | let _sorted = [] 319 | 320 | // get performances 321 | _.forEach(performance, function(obj) { 322 | let dateParts = obj.month_of_creation.split('-') 323 | let date = new Date(parseInt(dateParts[0], 10), 324 | parseInt(dateParts[1], 10) - 1, parseInt(dateParts[2], 10)) 325 | performances.push({creation_id: obj.month_of_creation, timestamp: date.getTime(), 326 | score: obj.score, valid: obj.valid}) 327 | }) 328 | 329 | // sort performances by period 330 | _sorted = _.sortBy(performances, 'timestamp') 331 | _.forEach(_sorted, function(obj) { 332 | scores.push(obj.score) 333 | valids.push(obj.valid) 334 | labels.push(makeLabel(obj.timestamp)) 335 | }) 336 | data = {scores: scores, valids: valids, labels: labels} 337 | return data 338 | } 339 | 340 | function makeScoreLinePayload(results, performance) { 341 | let chartData = makeChartData(performance) 342 | let scores = chartData.scores 343 | let valids = chartData.valids 344 | let labels = chartData.labels 345 | let data = { 346 | labels: labels, 347 | datasets: [ 348 | { 349 | label: "Score", 350 | fillColor: "rgba(122, 184, 0,0.2)", 351 | strokeColor: "rgba(122, 184, 0,1)", 352 | pointColor: "rgba(122, 184, 0,1)", 353 | pointStrokeColor: "#fff", 354 | pointHighlightFill: "#fff", 355 | pointHighlightStroke: "rgba(122, 184, 0,1)", 356 | data: scores 357 | }, 358 | { 359 | label: "Correct", 360 | fillColor: "rgba(119,119,119,0.2)", 361 | strokeColor: "rgba(119,119,119,1)", 362 | pointColor: "rgba(119,119,119,1)", 363 | pointStrokeColor: "#fff", 364 | pointHighlightFill: "#fff", 365 | pointHighlightStroke: "rgba(119,119,119,1)", 366 | data: valids 367 | } 368 | ] 369 | } 370 | 371 | let options = { 372 | scaleShowGridLines : true, 373 | scaleGridLineColor : "rgba(0,0,0,.05)", 374 | scaleGridLineWidth : 1, 375 | scaleShowHorizontalLines: true, 376 | scaleShowVerticalLines: true, 377 | bezierCurve : true, 378 | bezierCurveTension : 0.4, 379 | pointDot : true, 380 | pointDotRadius : 4, 381 | pointDotStrokeWidth : 1, 382 | pointHitDetectionRadius : 2, 383 | datasetStroke : true, 384 | datasetStrokeWidth : 2, 385 | datasetFill : false, 386 | scaleLabel: '<%=value%> %', 387 | scaleOverride: true, 388 | scaleSteps: 10, 389 | scaleStepWidth: 10, 390 | scaleStartValue: 0, 391 | animation: false, 392 | multiTooltipTemplate: '<%= datasetLabel %>: <%= value %> %' 393 | } 394 | 395 | return { 396 | data: data, 397 | options: options 398 | } 399 | } 400 | 401 | function makeLegend() { 402 | let ulStyle = { 403 | listStyleType: 'none', 404 | 'float': 'right' 405 | } 406 | let liStyle = { 407 | display: 'inline-block', 408 | marginRight: '10px' 409 | } 410 | let colorStyle = { 411 | display: 'inline-block', 412 | width: '22px', 413 | height: '13px', 414 | marginRight: '5px' 415 | } 416 | let scoreColStyle = _.cloneDeep(colorStyle) 417 | let validColStyle = _.cloneDeep(colorStyle) 418 | scoreColStyle.backgroundColor = 'rgba(122, 184, 0,1)' 419 | validColStyle.backgroundColor = 'rgba(119,119,119,1)' 420 | let textStyle = { 421 | verticalAlign: 'top', 422 | fontSize: '13px' 423 | } 424 | let score =
  • 425 | {'Score (%)'}
  • 426 | let valid =
  • 427 | {'Correct (%)'}
  • 428 | let legend = ( 429 |
      430 | {score} 431 | {valid} 432 |
    433 | ) 434 | return legend 435 | } 436 | 437 | export default { 438 | makeOverviewNumber, 439 | makeOverview, 440 | makeTableBody, 441 | makeScoreLinePayload, 442 | searchIn, 443 | makeLegend 444 | } 445 | -------------------------------------------------------------------------------- /app/ui/styles/_footer.scss: -------------------------------------------------------------------------------- 1 | /* footer */ 2 | .site-footer { 3 | $footer-color: white; 4 | $footer-link-color: transparentize($footer-color, .4); 5 | $footer-disclaimer-color: transparentize($footer-color, .6); 6 | 7 | background: $footer-background; 8 | width: 100%; 9 | position:relative; 10 | z-index:99; 11 | color:$footer-color; 12 | 13 | .container { 14 | // padding: $base-spacing * 2 $base-spacing; 15 | padding: 0.5 * $base-spacing $base-spacing 2 * $base-spacing $base-spacing; 16 | 17 | .database-version { 18 | padding: $base-spacing 0px; 19 | } 20 | 21 | .footer-logo { 22 | margin-right: 1em; 23 | margin-bottom: 1em; 24 | margin-top:-4px; 25 | 26 | @media (min-width: $screen-sm-min) { 27 | float: left; 28 | margin-bottom: 0; 29 | } 30 | } 31 | 32 | .footer-logo img { 33 | height: 2em; 34 | } 35 | 36 | ul { 37 | margin-bottom: 1em; 38 | @media (min-width: $screen-sm-min) { 39 | float: left; 40 | margin-left: 1em; 41 | margin-bottom: 0; 42 | } 43 | } 44 | 45 | ul li { 46 | padding-right: 1em; 47 | 48 | @media (min-width: $screen-sm-min) { 49 | display: inline; 50 | text-align: left; 51 | } 52 | } 53 | 54 | a { 55 | color: $footer-link-color; 56 | 57 | &:hover { 58 | color: transparentize($footer-color, 0); 59 | } 60 | 61 | &[rel~="external"]:after { 62 | display:none; 63 | } 64 | } 65 | 66 | .footer-links { 67 | 68 | @media (min-width: $screen-sm-min) { 69 | float: right; 70 | } 71 | 72 | li { 73 | font-size: .8em; 74 | font-weight: 400; 75 | } 76 | 77 | ul.footer-social { 78 | margin-top: 1em; 79 | 80 | @media (min-width: $screen-sm-min) { 81 | float: right; 82 | margin-top: 0; 83 | } 84 | 85 | li { 86 | float: left; 87 | font-size: 1em; 88 | padding-right: .7em; 89 | display:block; 90 | margin-bottom:$base-spacing; 91 | 92 | &:last-child { 93 | padding-right: 0; 94 | } 95 | } 96 | 97 | img { 98 | opacity: .7; 99 | height: 1.6em; 100 | padding: 1px; 101 | 102 | &:hover { 103 | opacity: 1; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | hr { 111 | clear:both; 112 | border-color:$footer-disclaimer-color; 113 | margin-bottom:$base-spacing; 114 | } 115 | 116 | p { 117 | clear:both; 118 | font-size:12px; 119 | color:$footer-disclaimer-color; 120 | } 121 | } 122 | 123 | .container + .site-footer { 124 | margin-top:$base-spacing * 2; 125 | } 126 | -------------------------------------------------------------------------------- /app/ui/styles/_theme.scss: -------------------------------------------------------------------------------- 1 | a, 2 | a:focus { 3 | outline: 0; 4 | color: $blue; 5 | } 6 | 7 | a.navbar-brand.has-icon { 8 | color: $gray; 9 | } 10 | 11 | .jumbotron { 12 | p { 13 | font-weight: 300; 14 | } 15 | 16 | & + .jumbotron { 17 | margin-top:-30px; 18 | } 19 | 20 | &:last-child { 21 | margin-bottom:0; 22 | } 23 | 24 | &.inverse { 25 | background-image: $header-gradient; 26 | background-color:$navbar-inverse-bg; 27 | color:#fff; 28 | 29 | p, 30 | a { 31 | color:#fff; 32 | } 33 | 34 | .dropdown-menu > li > a { 35 | color:$navbar-inverse-bg; 36 | } 37 | 38 | .form-control, 39 | .btn { 40 | border-color:$navbar-inverse-bg; 41 | color:$navbar-inverse-bg; 42 | @include placeholder(rgba($green,0.5)); 43 | } 44 | 45 | .btn-primary { 46 | background-color:#fff; 47 | } 48 | .btn-info { 49 | color:#fff; 50 | border-color:$gray-dark; 51 | } 52 | 53 | .well { 54 | background-color: rgba(0,0,0,0.03); 55 | border-color: darken($navbar-inverse-bg, 2); 56 | box-shadow: none; 57 | } 58 | 59 | .counter { 60 | @extend .counter.inverse; 61 | } 62 | } 63 | 64 | &.danger { 65 | background-color:$brand-danger; 66 | color:#fff; 67 | } 68 | &.warning { 69 | background-color:$brand-warning; 70 | } 71 | } 72 | 73 | table.table { 74 | thead tr th { 75 | background-color:$gray; 76 | color:#fff; 77 | } 78 | } 79 | 80 | $footer-background: $gray-dark; 81 | html { 82 | background-color:$footer-background; 83 | } 84 | 85 | .counter { 86 | margin-top:20px; 87 | 88 | .value > span { 89 | background-color:$gray-dark; 90 | font-weight:bold; 91 | /*font-size:114px;*/ 92 | display:inline-block; 93 | /*width:80px; 94 | height:128px;*/ 95 | text-align:center; 96 | margin-left:3px; 97 | margin-right:3px; 98 | /*line-height:124px;*/ 99 | border-radius:1px; 100 | position:relative; 101 | color:#fff; 102 | 103 | &:after { 104 | content:''; 105 | width: 100%; 106 | height:2px; 107 | display:block; 108 | position:absolute; 109 | top:50%; 110 | margin-top:-1px; 111 | background-color:#fff; 112 | } 113 | } 114 | 115 | .label { 116 | color:$gray-dark; 117 | font-size:20px; 118 | font-weight:normal; 119 | text-transform:uppercase; 120 | margin-top:15px; 121 | display:block; 122 | padding:0; 123 | } 124 | 125 | &.inverse { 126 | background-color:#FF0004; 127 | .value > span { 128 | background-color:#fff; 129 | color:$navbar-inverse-bg; 130 | 131 | &:after { 132 | background-color:$navbar-inverse-bg; 133 | } 134 | } 135 | 136 | .label { 137 | color:#fff; 138 | } 139 | } 140 | } 141 | 142 | 143 | h1, h2, h3, h4, h5, h6 { 144 | &.panel-heading { 145 | margin:0; 146 | } 147 | } 148 | 149 | .panel-heading { 150 | a { 151 | color:#fff; 152 | } 153 | } 154 | 155 | 156 | .nav > li > a { 157 | color:$gray-dark; 158 | } 159 | 160 | .nav-stacked > li { 161 | background-color:$gray-lighter; 162 | 163 | & + li { 164 | margin:0; 165 | } 166 | 167 | &:first-child { 168 | border-top:solid 15px $gray-lighter; 169 | } 170 | 171 | &:last-child { 172 | border-bottom:solid 15px $gray-lighter; 173 | } 174 | 175 | & > a:hover, 176 | & > a:focus { 177 | background-color: rgba(255,255,255,0.50); 178 | } 179 | 180 | &.active a { 181 | background-color:$brand-primary; 182 | color:#fff; 183 | } 184 | } 185 | 186 | 187 | .navbar-brand { 188 | 189 | .text { 190 | font-weight:900; 191 | text-transform:uppercase; 192 | letter-spacing:1px; 193 | position:relative; 194 | display:block; 195 | 196 | &:before { 197 | content:'Open Knowledge International'; 198 | display:block; 199 | font-size:10px; 200 | color:$brand-primary; 201 | margin-top:-2px; 202 | line-height:1; 203 | letter-spacing:2px; 204 | } 205 | } 206 | 207 | .glyphicon { 208 | $icon-colour:$gray; 209 | $icon-shadow: darken($icon-colour, 2); 210 | 211 | float:left; 212 | background-color:$icon-colour; 213 | border-radius:50%; 214 | padding:8px; 215 | margin-top:-7px; 216 | text-align:center; 217 | color:#fff; 218 | font-size:18px; 219 | width:35px; 220 | height:35px; 221 | line-height:19px; 222 | text-shadow: $icon-shadow 1px 1px, 223 | $icon-shadow 2px 2px, 224 | $icon-shadow 3px 3px, 225 | $icon-shadow 4px 4px, 226 | $icon-shadow 5px 5px, 227 | $icon-shadow 6px 6px, 228 | $icon-shadow 7px 7px, 229 | $icon-shadow 8px 8px, 230 | $icon-shadow 9px 9px, 231 | $icon-shadow 10px 10px, 232 | $icon-shadow 11px 11px, 233 | $icon-shadow 12px 12px; 234 | overflow:hidden; 235 | display:none; 236 | 237 | @media (min-width: $screen-sm-min) { 238 | display:block; 239 | } 240 | } 241 | 242 | &.has-icon { 243 | .text { 244 | @media (min-width: $screen-sm-min) { 245 | margin-left:42px; 246 | } 247 | } 248 | } 249 | 250 | &:hover { 251 | color:$brand-primary; 252 | } 253 | } 254 | 255 | .navbar-header { 256 | position:relative; 257 | 258 | .release.badge { 259 | position:absolute; 260 | left:100%; 261 | top:24px; 262 | margin-left:-12px; 263 | text-transform:uppercase; 264 | font-size:7px; 265 | background-color: $gray-light; 266 | padding: 2px 4px; 267 | opacity:0.3; 268 | display:none; 269 | 270 | @media (min-width: $grid-float-breakpoint) { 271 | display:block; 272 | } 273 | } 274 | } 275 | 276 | .navbar-toggle { 277 | background-color:$brand-primary; 278 | 279 | .icon-bar { 280 | background-color:#fff; 281 | } 282 | } 283 | 284 | .navbar { 285 | .container { 286 | position:relative; 287 | 288 | //nav 289 | .navbar-nav { 290 | 291 | & > .active > a, 292 | & > .active > a:hover, 293 | & > .active > a:focus { 294 | background-color: $brand-primary; 295 | color: #fff; 296 | } 297 | 298 | @media (min-width: $screen-sm-min) { 299 | float:right; 300 | font-size:16px; 301 | 302 | & > li > a { 303 | $vert-padding: 4px; 304 | $vert-margin: (($navbar-height - 20px) / 2) - $vert-padding; 305 | 306 | margin-bottom: $vert-margin; 307 | margin-top: $vert-margin; 308 | padding-top:$vert-padding; 309 | padding-bottom:$vert-padding; 310 | border-radius:10px + $vert-padding; 311 | } 312 | } 313 | } 314 | } 315 | 316 | &.navbar-inverse { 317 | background-image: $header-gradient; 318 | 319 | .navbar-brand { 320 | color:#fff; 321 | 322 | .text { 323 | &:before { 324 | color:#fff; 325 | } 326 | } 327 | 328 | .glyphicon { 329 | background-color:#fff; 330 | text-shadow: none; 331 | color:$navbar-inverse-bg; 332 | } 333 | } 334 | 335 | .release.badge { 336 | background-color:#fff; 337 | color:$navbar-inverse-bg; 338 | opacity:0.6; 339 | } 340 | 341 | .open-knowledge { 342 | background-position:center -200px; 343 | border-bottom-color:#2D2D2D; 344 | 345 | &:before { 346 | border-bottom-color:#000; 347 | } 348 | } 349 | 350 | .navbar-nav { 351 | & > .active > a, 352 | & > .active > a:hover, 353 | & > .active > a:focus { 354 | background-color: $navbar-inverse-link-active-bg; 355 | color: $navbar-inverse-link-active-color; 356 | } 357 | } 358 | 359 | & + div .jumbotron:first-child { 360 | @extend .jumbotron.inverse; 361 | } 362 | } 363 | } 364 | 365 | //Docs 366 | .docs { 367 | padding-bottom:20px; 368 | 369 | & > .container > .wrapper { 370 | @include make-row(); 371 | 372 | & > .nav { 373 | @include make-sm-column(3); 374 | 375 | & + .page-content { 376 | @include make-sm-column(9); 377 | 378 | .embed-responsive { 379 | margin-bottom:20px; 380 | } 381 | } 382 | } 383 | } 384 | } 385 | 386 | @import "footer"; 387 | -------------------------------------------------------------------------------- /app/ui/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $bootstrap-sass-asset-helper: false !default; 2 | // 3 | // Variables 4 | // -------------------------------------------------- 5 | 6 | 7 | //== Colors 8 | // 9 | //## Gray and brand colors for use across Bootstrap. 10 | 11 | $gray-base: #000 !default; 12 | $gray-darker: lighten($gray-base, 13.5%) !default; // #222 13 | $gray-dark: lighten($gray-base, 20%) !default; // #333 14 | $gray: lighten($gray-base, 33.5%) !default; // #555 15 | $gray-light: lighten($gray-base, 46.7%) !default; // #777 16 | $gray-lighter: lighten($gray-base, 93.5%) !default; // #eee 17 | 18 | $green: rgb(122, 184, 0); // #7ab800 19 | $red: rgb(255, 0, 0); 20 | $blue: rgb(0, 165, 224); 21 | 22 | $brand-primary: $gray-darker !default; 23 | $brand-success: rgba(85, 205, 40, 0.88) !default; 24 | $brand-info: $blue !default; 25 | $brand-warning: rgba(243, 217, 73, 0.79) !default; 26 | $brand-danger: rgba(255, 0, 0, 0.7) !default; 27 | 28 | 29 | //== Scaffolding 30 | // 31 | //## Settings for some of the most global styles. 32 | 33 | // ** Background color for ``. 34 | $body-bg: #fff !default; 35 | // ** Global text color on ``. 36 | $text-color: $gray-dark !default; 37 | 38 | // ** Global textual link color. 39 | $link-color: $blue !default; 40 | // ** Link hover color set via `darken()` function. 41 | $link-hover-color: darken($link-color, 15%) !default; 42 | // ** Link hover decoration. 43 | $link-hover-decoration: underline !default; 44 | 45 | 46 | //== Typography 47 | // 48 | //## Font, line-height, and color for body text, headings, and more. 49 | 50 | $font-family-sans-serif: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif !default; 51 | $font-family-serif: Georgia, "Times New Roman", Times, serif !default; 52 | // ** Default monospace fonts for ``, ``, and `
    `.
     53 | $font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
     54 | $font-family-base:        $font-family-sans-serif !default;
     55 | 
     56 | $font-size-base:          15px !default;
     57 | $font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
     58 | $font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
     59 | 
     60 | $font-size-h1:            floor(($font-size-base * 2.6)) !default; // ~36px
     61 | $font-size-h2:            floor(($font-size-base * 2.15)) !default; // ~30px
     62 | $font-size-h3:            ceil(($font-size-base * 1.7)) !default; // ~24px
     63 | $font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
     64 | $font-size-h5:            $font-size-base !default;
     65 | $font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
     66 | 
     67 | // ** Unit-less `line-height` for use in components like buttons.
     68 | $line-height-base:        1.428571429 !default; // 20/14
     69 | // ** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
     70 | $line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
     71 | 
     72 | // ** By default, this inherits from the ``.
     73 | $headings-font-family:    inherit !default;
     74 | $headings-font-weight:    500 !default;
     75 | $headings-line-height:    1.1 !default;
     76 | $headings-color:          inherit !default;
     77 | 
     78 | 
     79 | //== Iconography
     80 | //
     81 | //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
     82 | 
     83 | // ** Load fonts from this directory.
     84 | 
     85 | // [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
     86 | // [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
     87 | $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../bower_components/bootstrap-sass/assets/fonts/bootstrap/") !default;
     88 | 
     89 | // ** File name for all font files.
     90 | $icon-font-name:          "glyphicons-halflings-regular" !default;
     91 | // ** Element ID within SVG icon file.
     92 | $icon-font-svg-id:        "glyphicons_halflingsregular" !default;
     93 | 
     94 | 
     95 | //== Components
     96 | //
     97 | //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
     98 | 
     99 | $padding-base-vertical:     6px !default;
    100 | $padding-base-horizontal:   12px !default;
    101 | 
    102 | $padding-large-vertical:    10px !default;
    103 | $padding-large-horizontal:  16px !default;
    104 | 
    105 | $padding-small-vertical:    5px !default;
    106 | $padding-small-horizontal:  10px !default;
    107 | 
    108 | $padding-xs-vertical:       1px !default;
    109 | $padding-xs-horizontal:     5px !default;
    110 | 
    111 | $line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
    112 | $line-height-small:         1.5 !default;
    113 | 
    114 | $border-radius-base:        4px !default;
    115 | $border-radius-large:       6px !default;
    116 | $border-radius-small:       3px !default;
    117 | 
    118 | // ** Global color for active items (e.g., navs or dropdowns).
    119 | $component-active-color:    #fff !default;
    120 | // ** Global background color for active items (e.g., navs or dropdowns).
    121 | $component-active-bg:       $brand-primary !default;
    122 | 
    123 | // ** Width of the `border` for generating carets that indicator dropdowns.
    124 | $caret-width-base:          4px !default;
    125 | // ** Carets increase slightly in size for larger components.
    126 | $caret-width-large:         5px !default;
    127 | 
    128 | 
    129 | //== Tables
    130 | //
    131 | //## Customizes the `.table` component with basic values, each used across all table variations.
    132 | 
    133 | // ** Padding for `
    {value}{value}{value + ' %'}{displayed_period}{value.charAt(0).toUpperCase() + value.slice(1).replace('-', ' ')}{data_file_name}{displayed_period}{}{'Details'}See schemaSchema unavailable{value}`s and ``s. 134 | $table-cell-padding: 8px !default; 135 | // ** Padding for cells in `.table-condensed`. 136 | $table-condensed-cell-padding: 5px !default; 137 | 138 | // ** Default background color used for all tables. 139 | $table-bg: transparent !default; 140 | // ** Background color used for `.table-striped`. 141 | $table-bg-accent: #f9f9f9 !default; 142 | // ** Background color used for `.table-hover`. 143 | $table-bg-hover: #f5f5f5 !default; 144 | $table-bg-active: $table-bg-hover !default; 145 | 146 | // ** Border color for table and cell borders. 147 | $table-border-color: #ddd !default; 148 | 149 | 150 | //== Buttons 151 | // 152 | //## For each of Bootstrap's buttons, define text, background and border color. 153 | 154 | $btn-font-weight: normal !default; 155 | 156 | $btn-default-color: #333 !default; 157 | $btn-default-bg: #fff !default; 158 | $btn-default-border: #ccc !default; 159 | 160 | $btn-primary-color: #fff !default; 161 | $btn-primary-bg: $brand-primary !default; 162 | $btn-primary-border: darken($btn-primary-bg, 5%) !default; 163 | 164 | $btn-success-color: #fff !default; 165 | $btn-success-bg: $brand-success !default; 166 | $btn-success-border: darken($btn-success-bg, 5%) !default; 167 | 168 | $btn-info-color: #fff !default; 169 | $btn-info-bg: $gray-dark !default; 170 | $btn-info-border: darken($btn-info-bg, 5%) !default; 171 | 172 | $btn-warning-color: #fff !default; 173 | $btn-warning-bg: $brand-warning !default; 174 | $btn-warning-border: darken($btn-warning-bg, 5%) !default; 175 | 176 | $btn-danger-color: #fff !default; 177 | $btn-danger-bg: $brand-danger !default; 178 | $btn-danger-border: darken($btn-danger-bg, 5%) !default; 179 | 180 | $btn-link-disabled-color: $gray-light !default; 181 | 182 | 183 | //== Forms 184 | // 185 | //## 186 | 187 | // ** `` background color 188 | $input-bg: #fff !default; 189 | // ** `` background color 190 | $input-bg-disabled: $gray-lighter !default; 191 | 192 | // ** Text color for ``s 193 | $input-color: $gray !default; 194 | // ** `` border color 195 | $input-border: #ccc !default; 196 | 197 | // TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 198 | // ** Default `.form-control` border radius 199 | // This has no effect on ``s in CSS. 200 | $input-border-radius: $border-radius-base !default; 201 | // ** Large `.form-control` border radius 202 | $input-border-radius-large: $border-radius-large !default; 203 | // ** Small `.form-control` border radius 204 | $input-border-radius-small: $border-radius-small !default; 205 | 206 | // ** Border color for inputs on focus 207 | $input-border-focus: #66afe9 !default; 208 | 209 | // ** Placeholder text color 210 | $input-color-placeholder: #999 !default; 211 | 212 | // ** Default `.form-control` height 213 | $input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) !default; 214 | // ** Large `.form-control` height 215 | $input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default; 216 | // ** Small `.form-control` height 217 | $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default; 218 | 219 | $legend-color: $gray-dark !default; 220 | $legend-border-color: #e5e5e5 !default; 221 | 222 | // ** Background color for textual input addons 223 | $input-group-addon-bg: $gray-lighter !default; 224 | // ** Border color for textual input addons 225 | $input-group-addon-border-color: $input-border !default; 226 | 227 | // ** Disabled cursor for form controls and buttons. 228 | $cursor-disabled: not-allowed !default; 229 | 230 | 231 | //== Dropdowns 232 | // 233 | //## Dropdown menu container and contents. 234 | 235 | // ** Background for the dropdown menu. 236 | $dropdown-bg: #fff !default; 237 | // ** Dropdown menu `border-color`. 238 | $dropdown-border: rgba(0,0,0,.15) !default; 239 | // ** Dropdown menu `border-color` **for IE8**. 240 | $dropdown-fallback-border: #ccc !default; 241 | // ** Divider color for between dropdown items. 242 | $dropdown-divider-bg: #e5e5e5 !default; 243 | 244 | // ** Dropdown link text color. 245 | $dropdown-link-color: $gray-dark !default; 246 | // ** Hover color for dropdown links. 247 | $dropdown-link-hover-color: darken($gray-dark, 5%) !default; 248 | // ** Hover background for dropdown links. 249 | $dropdown-link-hover-bg: #f5f5f5 !default; 250 | 251 | // ** Active dropdown menu item text color. 252 | $dropdown-link-active-color: $component-active-color !default; 253 | // ** Active dropdown menu item background color. 254 | $dropdown-link-active-bg: $component-active-bg !default; 255 | 256 | // ** Disabled dropdown menu item background color. 257 | $dropdown-link-disabled-color: $gray-light !default; 258 | 259 | // ** Text color for headers within dropdown menus. 260 | $dropdown-header-color: $gray-light !default; 261 | 262 | // ** Deprecated `$dropdown-caret-color` as of v3.1.0 263 | $dropdown-caret-color: #000 !default; 264 | 265 | 266 | //-- Z-index master list 267 | // 268 | // Warning: Avoid customizing these values. They're used for a bird's eye view 269 | // of components dependent on the z-axis and are designed to all work together. 270 | // 271 | // Note: These variables are not generated into the Customizer. 272 | 273 | $zindex-navbar: 1000 !default; 274 | $zindex-dropdown: 1000 !default; 275 | $zindex-popover: 1060 !default; 276 | $zindex-tooltip: 1070 !default; 277 | $zindex-navbar-fixed: 1030 !default; 278 | $zindex-modal: 1040 !default; 279 | 280 | 281 | //== Media queries breakpoints 282 | // 283 | //## Define the breakpoints at which your layout will change, adapting to different screen sizes. 284 | 285 | // Extra small screen / phone 286 | // ** Deprecated `$screen-xs` as of v3.0.1 287 | $screen-xs: 480px !default; 288 | // ** Deprecated `$screen-xs-min` as of v3.2.0 289 | $screen-xs-min: $screen-xs !default; 290 | // ** Deprecated `$screen-phone` as of v3.0.1 291 | $screen-phone: $screen-xs-min !default; 292 | 293 | // Small screen / tablet 294 | // ** Deprecated `$screen-sm` as of v3.0.1 295 | $screen-sm: 768px !default; 296 | $screen-sm-min: $screen-sm !default; 297 | // ** Deprecated `$screen-tablet` as of v3.0.1 298 | $screen-tablet: $screen-sm-min !default; 299 | 300 | // Medium screen / desktop 301 | // ** Deprecated `$screen-md` as of v3.0.1 302 | $screen-md: 992px !default; 303 | $screen-md-min: $screen-md !default; 304 | // ** Deprecated `$screen-desktop` as of v3.0.1 305 | $screen-desktop: $screen-md-min !default; 306 | 307 | // Large screen / wide desktop 308 | // ** Deprecated `$screen-lg` as of v3.0.1 309 | $screen-lg: 1200px !default; 310 | $screen-lg-min: $screen-lg !default; 311 | // ** Deprecated `$screen-lg-desktop` as of v3.0.1 312 | $screen-lg-desktop: $screen-lg-min !default; 313 | 314 | // So media queries don't overlap when required, provide a maximum 315 | $screen-xs-max: ($screen-sm-min - 1) !default; 316 | $screen-sm-max: ($screen-md-min - 1) !default; 317 | $screen-md-max: ($screen-lg-min - 1) !default; 318 | 319 | 320 | //== Grid system 321 | // 322 | //## Define your custom responsive grid. 323 | 324 | // ** Number of columns in the grid. 325 | $grid-columns: 12 !default; 326 | // ** Padding between columns. Gets divided in half for the left and right. 327 | $grid-gutter-width: 40px !default; 328 | // Navbar collapse 329 | // ** Point at which the navbar becomes uncollapsed. 330 | $grid-float-breakpoint: $screen-md-min !default; 331 | // ** Point at which the navbar begins collapsing. 332 | $grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default; 333 | 334 | 335 | //== Container sizes 336 | // 337 | //## Define the maximum width of `.container` for different screen sizes. 338 | 339 | // Small screen / tablet 340 | $container-tablet: (720px + $grid-gutter-width) !default; 341 | // ** For `$screen-sm-min` and up. 342 | $container-sm: $container-tablet !default; 343 | 344 | // Medium screen / desktop 345 | $container-desktop: (940px + $grid-gutter-width) !default; 346 | // ** For `$screen-md-min` and up. 347 | $container-md: $container-desktop !default; 348 | 349 | // Large screen / wide desktop 350 | $container-large-desktop: (1140px + $grid-gutter-width) !default; 351 | // ** For `$screen-lg-min` and up. 352 | $container-lg: $container-large-desktop !default; 353 | 354 | 355 | //== Navbar 356 | // 357 | //## 358 | 359 | // Basics of a navbar 360 | $navbar-height: 72px !default; 361 | $navbar-margin-bottom: 0 !default; 362 | $navbar-border-radius: $border-radius-base !default; 363 | $navbar-padding-horizontal: floor(($grid-gutter-width / 2)) !default; 364 | $navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2) !default; 365 | $navbar-collapse-max-height: 340px !default; 366 | 367 | $navbar-default-color: $gray-dark !default; 368 | $navbar-default-bg: $gray-lighter !default; 369 | $navbar-default-border: darken($navbar-default-bg, 3%) !default; 370 | 371 | // Navbar links 372 | $navbar-default-link-color: #777 !default; 373 | $navbar-default-link-hover-color: #333 !default; 374 | $navbar-default-link-hover-bg: transparent !default; 375 | $navbar-default-link-active-color: #555 !default; 376 | $navbar-default-link-active-bg: darken($navbar-default-bg, 6.5%) !default; 377 | $navbar-default-link-disabled-color: #ccc !default; 378 | $navbar-default-link-disabled-bg: transparent !default; 379 | 380 | // Navbar brand label 381 | $navbar-default-brand-color: $brand-primary !default; 382 | $navbar-default-brand-hover-color: $navbar-default-brand-color !default; 383 | $navbar-default-brand-hover-bg: transparent !default; 384 | 385 | // Navbar toggle 386 | $navbar-default-toggle-hover-bg: #ddd !default; 387 | $navbar-default-toggle-icon-bar-bg: #888 !default; 388 | $navbar-default-toggle-border-color: #ddd !default; 389 | 390 | 391 | // Inverted navbar 392 | // Reset inverted navbar basics 393 | $navbar-inverse-color: #fff !default; 394 | $navbar-inverse-bg: darken($green, 5) !default; 395 | $navbar-inverse-border: darken($navbar-inverse-bg, 2%) !default; 396 | 397 | // Inverted navbar links 398 | $navbar-inverse-link-color: rgba(#fff, 0.9) !default; 399 | $navbar-inverse-link-hover-color: #fff !default; 400 | $navbar-inverse-link-hover-bg: transparent !default; 401 | $navbar-inverse-link-active-color: $navbar-inverse-bg !default; 402 | $navbar-inverse-link-active-bg: #fff !default; 403 | $navbar-inverse-link-disabled-color: #444 !default; 404 | $navbar-inverse-link-disabled-bg: transparent !default; 405 | 406 | // Inverted navbar brand label 407 | $navbar-inverse-brand-color: $navbar-inverse-link-color !default; 408 | $navbar-inverse-brand-hover-color: #fff !default; 409 | $navbar-inverse-brand-hover-bg: transparent !default; 410 | 411 | // Inverted navbar toggle 412 | $navbar-inverse-toggle-hover-bg: #333 !default; 413 | $navbar-inverse-toggle-icon-bar-bg: #fff !default; 414 | $navbar-inverse-toggle-border-color: $navbar-inverse-border !default; 415 | 416 | 417 | //== Navs 418 | // 419 | //## 420 | 421 | //=== Shared nav styles 422 | $nav-link-padding: 10px 15px !default; 423 | $nav-link-hover-bg: $gray-lighter !default; 424 | 425 | $nav-disabled-link-color: $gray-light !default; 426 | $nav-disabled-link-hover-color: $gray-light !default; 427 | 428 | //== Tabs 429 | $nav-tabs-border-color: #ddd !default; 430 | 431 | $nav-tabs-link-hover-border-color: $gray-lighter !default; 432 | 433 | $nav-tabs-active-link-hover-bg: $body-bg !default; 434 | $nav-tabs-active-link-hover-color: $gray !default; 435 | $nav-tabs-active-link-hover-border-color: #ddd !default; 436 | 437 | $nav-tabs-justified-link-border-color: #ddd !default; 438 | $nav-tabs-justified-active-link-border-color: $body-bg !default; 439 | 440 | //== Pills 441 | $nav-pills-border-radius: $border-radius-base !default; 442 | $nav-pills-active-link-hover-bg: $component-active-bg !default; 443 | $nav-pills-active-link-hover-color: $component-active-color !default; 444 | 445 | 446 | //== Pagination 447 | // 448 | //## 449 | 450 | $pagination-color: $link-color !default; 451 | $pagination-bg: #fff !default; 452 | $pagination-border: #ddd !default; 453 | 454 | $pagination-hover-color: $link-hover-color !default; 455 | $pagination-hover-bg: $gray-lighter !default; 456 | $pagination-hover-border: #ddd !default; 457 | 458 | $pagination-active-color: #fff !default; 459 | $pagination-active-bg: $brand-primary !default; 460 | $pagination-active-border: $brand-primary !default; 461 | 462 | $pagination-disabled-color: $gray-light !default; 463 | $pagination-disabled-bg: #fff !default; 464 | $pagination-disabled-border: #ddd !default; 465 | 466 | 467 | //== Pager 468 | // 469 | //## 470 | 471 | $pager-bg: $pagination-bg !default; 472 | $pager-border: $pagination-border !default; 473 | $pager-border-radius: 15px !default; 474 | 475 | $pager-hover-bg: $pagination-hover-bg !default; 476 | 477 | $pager-active-bg: $pagination-active-bg !default; 478 | $pager-active-color: $pagination-active-color !default; 479 | 480 | $pager-disabled-color: $pagination-disabled-color !default; 481 | 482 | 483 | //== Jumbotron 484 | // 485 | //## 486 | 487 | $jumbotron-padding: 30px !default; 488 | $jumbotron-color: inherit !default; 489 | $jumbotron-bg: $gray-lighter !default; 490 | $jumbotron-heading-color: inherit !default; 491 | $jumbotron-font-size: ceil(($font-size-base * 1.5)) !default; 492 | 493 | 494 | //== Form states and alerts 495 | // 496 | //## Define colors for form feedback states and, by default, alerts. 497 | 498 | $state-success-text: #3c763d !default; 499 | $state-success-bg: #dff0d8 !default; 500 | $state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) !default; 501 | 502 | $state-info-text: #31708f !default; 503 | $state-info-bg: #d9edf7 !default; 504 | $state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) !default; 505 | 506 | $state-warning-text: #8a6d3b !default; 507 | $state-warning-bg: #fcf8e3 !default; 508 | $state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) !default; 509 | 510 | $state-danger-text: #a94442 !default; 511 | $state-danger-bg: #f2dede !default; 512 | $state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) !default; 513 | 514 | 515 | //== Tooltips 516 | // 517 | //## 518 | 519 | // ** Tooltip max width 520 | $tooltip-max-width: 200px !default; 521 | // ** Tooltip text color 522 | $tooltip-color: #fff !default; 523 | // ** Tooltip background color 524 | $tooltip-bg: #000 !default; 525 | $tooltip-opacity: .9 !default; 526 | 527 | // ** Tooltip arrow width 528 | $tooltip-arrow-width: 5px !default; 529 | // ** Tooltip arrow color 530 | $tooltip-arrow-color: $tooltip-bg !default; 531 | 532 | 533 | //== Popovers 534 | // 535 | //## 536 | 537 | // ** Popover body background color 538 | $popover-bg: #fff !default; 539 | // ** Popover maximum width 540 | $popover-max-width: 276px !default; 541 | // ** Popover border color 542 | $popover-border-color: rgba(0,0,0,.2) !default; 543 | // ** Popover fallback border color 544 | $popover-fallback-border-color: #ccc !default; 545 | 546 | // ** Popover title background color 547 | $popover-title-bg: darken($popover-bg, 3%) !default; 548 | 549 | // ** Popover arrow width 550 | $popover-arrow-width: 10px !default; 551 | // ** Popover arrow color 552 | $popover-arrow-color: $popover-bg !default; 553 | 554 | // ** Popover outer arrow width 555 | $popover-arrow-outer-width: ($popover-arrow-width + 1) !default; 556 | // ** Popover outer arrow color 557 | $popover-arrow-outer-color: fade_in($popover-border-color, 0.05) !default; 558 | // ** Popover outer arrow fallback color 559 | $popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%) !default; 560 | 561 | 562 | //== Labels 563 | // 564 | //## 565 | 566 | // ** Default label background color 567 | $label-default-bg: $gray-light !default; 568 | // ** Primary label background color 569 | $label-primary-bg: $brand-primary !default; 570 | // ** Success label background color 571 | $label-success-bg: $brand-success !default; 572 | // ** Info label background color 573 | $label-info-bg: $brand-info !default; 574 | // ** Warning label background color 575 | $label-warning-bg: $brand-warning !default; 576 | // ** Danger label background color 577 | $label-danger-bg: $brand-danger !default; 578 | 579 | // ** Default label text color 580 | $label-color: #fff !default; 581 | // ** Default text color of a linked label 582 | $label-link-hover-color: #fff !default; 583 | 584 | 585 | //== Modals 586 | // 587 | //## 588 | 589 | // ** Padding applied to the modal body 590 | $modal-inner-padding: 15px !default; 591 | 592 | // ** Padding applied to the modal title 593 | $modal-title-padding: 15px !default; 594 | // ** Modal title line-height 595 | $modal-title-line-height: $line-height-base !default; 596 | 597 | // ** Background color of modal content area 598 | $modal-content-bg: #fff !default; 599 | // ** Modal content border color 600 | $modal-content-border-color: rgba(0,0,0,.2) !default; 601 | // ** Modal content border color **for IE8** 602 | $modal-content-fallback-border-color: #999 !default; 603 | 604 | // ** Modal backdrop background color 605 | $modal-backdrop-bg: #000 !default; 606 | // ** Modal backdrop opacity 607 | $modal-backdrop-opacity: .5 !default; 608 | // ** Modal header border color 609 | $modal-header-border-color: #e5e5e5 !default; 610 | // ** Modal footer border color 611 | $modal-footer-border-color: $modal-header-border-color !default; 612 | 613 | $modal-lg: 900px !default; 614 | $modal-md: 600px !default; 615 | $modal-sm: 300px !default; 616 | 617 | 618 | //== Alerts 619 | // 620 | //## Define alert colors, border radius, and padding. 621 | 622 | $alert-padding: 15px !default; 623 | $alert-border-radius: $border-radius-base !default; 624 | $alert-link-font-weight: bold !default; 625 | 626 | $alert-success-bg: $state-success-bg !default; 627 | $alert-success-text: $state-success-text !default; 628 | $alert-success-border: $state-success-border !default; 629 | 630 | $alert-info-bg: $state-info-bg !default; 631 | $alert-info-text: $state-info-text !default; 632 | $alert-info-border: $state-info-border !default; 633 | 634 | $alert-warning-bg: $state-warning-bg !default; 635 | $alert-warning-text: $state-warning-text !default; 636 | $alert-warning-border: $state-warning-border !default; 637 | 638 | $alert-danger-bg: $state-danger-bg !default; 639 | $alert-danger-text: $state-danger-text !default; 640 | $alert-danger-border: $state-danger-border !default; 641 | 642 | 643 | //== Progress bars 644 | // 645 | //## 646 | 647 | // ** Background color of the whole progress component 648 | $progress-bg: #f5f5f5 !default; 649 | // ** Progress bar text color 650 | $progress-bar-color: #fff !default; 651 | // ** Variable for setting rounded corners on progress bar. 652 | $progress-border-radius: $border-radius-base !default; 653 | 654 | // ** Default progress bar color 655 | $progress-bar-bg: $brand-primary !default; 656 | // ** Success progress bar color 657 | $progress-bar-success-bg: $brand-success !default; 658 | // ** Warning progress bar color 659 | $progress-bar-warning-bg: $brand-warning !default; 660 | // ** Danger progress bar color 661 | $progress-bar-danger-bg: $brand-danger !default; 662 | // ** Info progress bar color 663 | $progress-bar-info-bg: $brand-info !default; 664 | 665 | 666 | //== List group 667 | // 668 | //## 669 | 670 | // ** Background color on `.list-group-item` 671 | $list-group-bg: #fff !default; 672 | // ** `.list-group-item` border color 673 | $list-group-border: #ddd !default; 674 | // ** List group border radius 675 | $list-group-border-radius: $border-radius-base !default; 676 | 677 | // ** Background color of single list items on hover 678 | $list-group-hover-bg: #f5f5f5 !default; 679 | // ** Text color of active list items 680 | $list-group-active-color: $component-active-color !default; 681 | // ** Background color of active list items 682 | $list-group-active-bg: $component-active-bg !default; 683 | // ** Border color of active list elements 684 | $list-group-active-border: $list-group-active-bg !default; 685 | // ** Text color for content within active list items 686 | $list-group-active-text-color: lighten($list-group-active-bg, 40%) !default; 687 | 688 | // ** Text color of disabled list items 689 | $list-group-disabled-color: $gray-light !default; 690 | // ** Background color of disabled list items 691 | $list-group-disabled-bg: $gray-lighter !default; 692 | // ** Text color for content within disabled list items 693 | $list-group-disabled-text-color: $list-group-disabled-color !default; 694 | 695 | $list-group-link-color: #555 !default; 696 | $list-group-link-hover-color: $list-group-link-color !default; 697 | $list-group-link-heading-color: #333 !default; 698 | 699 | 700 | //== Panels 701 | // 702 | //## 703 | 704 | $panel-bg: #fff !default; 705 | $panel-body-padding: 15px !default; 706 | $panel-heading-padding: 15px 15px !default; 707 | $panel-footer-padding: $panel-heading-padding !default; 708 | $panel-border-radius: 2px !default; 709 | 710 | // ** Border color for elements within panels 711 | $panel-inner-border: #ddd !default; 712 | $panel-footer-bg: #f5f5f5 !default; 713 | 714 | $panel-default-text: #fff !default; 715 | $panel-default-border: $gray-dark !default; 716 | $panel-default-heading-bg: $gray-dark !default; 717 | 718 | $panel-primary-text: #fff !default; 719 | $panel-primary-border: $brand-primary !default; 720 | $panel-primary-heading-bg: $brand-primary !default; 721 | 722 | $panel-success-text: $state-success-text !default; 723 | $panel-success-border: $state-success-border !default; 724 | $panel-success-heading-bg: $state-success-bg !default; 725 | 726 | $panel-info-text: $state-info-text !default; 727 | $panel-info-border: $state-info-border !default; 728 | $panel-info-heading-bg: $state-info-bg !default; 729 | 730 | $panel-warning-text: $state-warning-text !default; 731 | $panel-warning-border: $state-warning-border !default; 732 | $panel-warning-heading-bg: $state-warning-bg !default; 733 | 734 | $panel-danger-text: $state-danger-text !default; 735 | $panel-danger-border: $state-danger-border !default; 736 | $panel-danger-heading-bg: $state-danger-bg !default; 737 | 738 | 739 | //== Thumbnails 740 | // 741 | //## 742 | 743 | // ** Padding around the thumbnail image 744 | $thumbnail-padding: 4px !default; 745 | // ** Thumbnail background color 746 | $thumbnail-bg: $body-bg !default; 747 | // ** Thumbnail border color 748 | $thumbnail-border: #ddd !default; 749 | // ** Thumbnail border radius 750 | $thumbnail-border-radius: $border-radius-base !default; 751 | 752 | // ** Custom text color for thumbnail captions 753 | $thumbnail-caption-color: $text-color !default; 754 | // ** Padding around the thumbnail caption 755 | $thumbnail-caption-padding: 9px !default; 756 | 757 | 758 | //== Wells 759 | // 760 | //## 761 | 762 | $well-bg: #f5f5f5 !default; 763 | $well-border: darken($well-bg, 7%) !default; 764 | 765 | 766 | //== Badges 767 | // 768 | //## 769 | 770 | $badge-color: #fff !default; 771 | // ** Linked badge text color on hover 772 | $badge-link-hover-color: #fff !default; 773 | $badge-bg: $gray-light !default; 774 | 775 | // ** Badge text color in active nav link 776 | $badge-active-color: $link-color !default; 777 | // ** Badge background color in active nav link 778 | $badge-active-bg: #fff !default; 779 | 780 | $badge-font-weight: bold !default; 781 | $badge-line-height: 1 !default; 782 | $badge-border-radius: 10px !default; 783 | 784 | 785 | //== Breadcrumbs 786 | // 787 | //## 788 | 789 | $breadcrumb-padding-vertical: 8px !default; 790 | $breadcrumb-padding-horizontal: 15px !default; 791 | // ** Breadcrumb background color 792 | $breadcrumb-bg: #f5f5f5 !default; 793 | // ** Breadcrumb text color 794 | $breadcrumb-color: #ccc !default; 795 | // ** Text color of current page in the breadcrumb 796 | $breadcrumb-active-color: $gray-light !default; 797 | // ** Textual separator for between breadcrumb elements 798 | $breadcrumb-separator: "/" !default; 799 | 800 | 801 | //== Carousel 802 | // 803 | //## 804 | 805 | $carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6) !default; 806 | 807 | $carousel-control-color: #fff !default; 808 | $carousel-control-width: 15% !default; 809 | $carousel-control-opacity: .5 !default; 810 | $carousel-control-font-size: 20px !default; 811 | 812 | $carousel-indicator-active-bg: #fff !default; 813 | $carousel-indicator-border-color: #fff !default; 814 | 815 | $carousel-caption-color: #fff !default; 816 | 817 | 818 | //== Close 819 | // 820 | //## 821 | 822 | $close-font-weight: bold !default; 823 | $close-color: #000 !default; 824 | $close-text-shadow: 0 1px 0 #fff !default; 825 | 826 | 827 | //== Code 828 | // 829 | //## 830 | 831 | $code-color: #c7254e !default; 832 | $code-bg: #f9f2f4 !default; 833 | 834 | $kbd-color: #fff !default; 835 | $kbd-bg: #333 !default; 836 | 837 | $pre-bg: #f5f5f5 !default; 838 | $pre-color: $gray-dark !default; 839 | $pre-border-color: #ccc !default; 840 | $pre-scrollable-max-height: 340px !default; 841 | 842 | 843 | //== Type 844 | // 845 | //## 846 | 847 | // ** Horizontal offset for forms and lists. 848 | $component-offset-horizontal: 180px !default; 849 | // ** Text muted color 850 | $text-muted: $gray-light !default; 851 | // ** Abbreviations and acronyms border color 852 | $abbr-border-color: $gray-light !default; 853 | // ** Headings small color 854 | $headings-small-color: $gray-light !default; 855 | // ** Blockquote small color 856 | $blockquote-small-color: $gray-light !default; 857 | // ** Blockquote font size 858 | $blockquote-font-size: ($font-size-base * 1.25) !default; 859 | // ** Blockquote border color 860 | $blockquote-border-color: $gray-lighter !default; 861 | // ** Page header border color 862 | $page-header-border-color: $gray-lighter !default; 863 | // ** Width of horizontal description list titles 864 | $dl-horizontal-offset: $component-offset-horizontal !default; 865 | // ** Horizontal line color. 866 | $hr-border: $gray-lighter !default; 867 | 868 | 869 | -------------------------------------------------------------------------------- /app/ui/styles/app.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | // # TODO: it is __VERY__ annoying that sass doesn't support string interpolation in @imports!!! 4 | // https://github.com/sass/sass/issues/49 is there another solution we can use? 5 | 6 | // Fonts 7 | @import url(http://fonts.googleapis.com/css?family=Lato:300,400,700,900,400italic); 8 | @import url("//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css"); 9 | 10 | // Our variables 11 | @import "variables"; 12 | 13 | //Bootstrap 14 | 15 | // Core variables and mixins 16 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables"; 17 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins"; 18 | 19 | // Reset and dependencies 20 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/normalize"; 21 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/print"; 22 | //@import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/glyphicons"; 23 | 24 | // Core CSS 25 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/scaffolding"; 26 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/type"; 27 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/code"; 28 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/grid"; 29 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/tables"; 30 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/forms"; 31 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/buttons"; 32 | 33 | // Components 34 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/component-animations"; 35 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/dropdowns"; 36 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/button-groups"; 37 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/input-groups"; 38 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/navs"; 39 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/navbar"; 40 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/breadcrumbs"; 41 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/pagination"; 42 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/pager"; 43 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/labels"; 44 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/badges"; 45 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/jumbotron"; 46 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/thumbnails"; 47 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/alerts"; 48 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/progress-bars"; 49 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/media"; 50 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/list-group"; 51 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/panels"; 52 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-embed"; 53 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/wells"; 54 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/close"; 55 | 56 | // Components w/ JavaScript 57 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/modals"; 58 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/tooltip"; 59 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/popovers"; 60 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/carousel"; 61 | 62 | // Utility classes 63 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/utilities"; 64 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities"; 65 | 66 | 67 | // Theme 68 | $base-spacing: 20px; 69 | $header-gradient: radial-gradient(circle at 50% top, rgba($green,0.6) 0%,rgba($green,0) 75%),radial-gradient(circle at right top, $green 0%,rgba($green,0) 57%); 70 | @import "theme"; 71 | 72 | // Custom 73 | @import "dashboard"; 74 | 75 | .is-fetching { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | width: 100%; 80 | height: 100%; 81 | font-size: 80px; 82 | min-height: 500px; 83 | background-color: #eee; 84 | } 85 | 86 | .explanation p, .explanation ul { 87 | font-size: 18px !important; 88 | font-weight: 300; 89 | } 90 | 91 | .dashboard .publishers { 92 | padding-bottom: 30px; 93 | } 94 | 95 | .ok-ribbon { 96 | position: absolute !important; 97 | } 98 | 99 | .navbar .container .navbar-nav { 100 | margin-right: 140px; 101 | } 102 | 103 | .jumbotron.inverse { 104 | background-color: #69b93b; 105 | background-image: none; 106 | } -------------------------------------------------------------------------------- /app/ui/styles/dashboard.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins"; 3 | //spend publishing dashboard 4 | 5 | .dashboard { 6 | .overview { 7 | @include make-row(); 8 | padding:0; 9 | text-align:center; 10 | 11 | li { 12 | //@include make-md-column(3); 13 | display:block; 14 | text-align:center; 15 | 16 | @media (min-width: $screen-md-min) { 17 | display: inline-block; 18 | } 19 | 20 | .value { 21 | display:block; 22 | } 23 | } 24 | } 25 | 26 | .line-chart { 27 | padding-bottom:30px; 28 | 29 | .intro { 30 | @include make-row(); 31 | position:relative; 32 | 33 | .text { 34 | @include make-md-column(8); 35 | } 36 | 37 | .more { 38 | @include make-md-column(4); 39 | 40 | @media (min-width: $screen-md-min) { 41 | text-align:right; 42 | position:absolute; 43 | bottom:20px; 44 | right:0; 45 | } 46 | } 47 | } 48 | 49 | #lineChart { 50 | width: 100% !important; 51 | max-width: 1140px !important; 52 | height: 300px !important; 53 | } 54 | } 55 | 56 | .publishers { 57 | background-color:$gray-lighter; 58 | padding-top:30px; 59 | 60 | .intro { 61 | @include make-row(); 62 | padding-bottom:10px; 63 | position:relative; 64 | 65 | .text { 66 | @include make-md-column(8); 67 | } 68 | 69 | .more { 70 | @include make-md-column(4); 71 | 72 | @media (min-width: $screen-md-min) { 73 | text-align:right; 74 | position:absolute; 75 | bottom:20px; 76 | right:0; 77 | } 78 | 79 | .download { 80 | margin-left: 20px; 81 | } 82 | } 83 | } 84 | 85 | table { 86 | background-color:#fff; 87 | margin-bottom:0; 88 | overflow:hidden; 89 | border-radius:$border-radius-base $border-radius-base 0 0; 90 | 91 | & > thead > tr > th:first-child { 92 | border-radius:$border-radius-base 0 0 0; 93 | } 94 | & > thead > tr > th:last-child { 95 | border-radius:0 $border-radius-base 0 0; 96 | } 97 | 98 | & > tbody > tr > td, 99 | & > tbody > tr > th { 100 | background-color:transparent; 101 | } 102 | .score { 103 | text-align:center; 104 | } 105 | td.success.score, td.success.date { 106 | background-color:$brand-success; 107 | } 108 | td.score-99p.score { 109 | background-color: #86D522; 110 | } 111 | td.score-80p.score { 112 | background-color: #C0DD1C; 113 | } 114 | td.score-60p.score, td.warning.date { 115 | background-color:$brand-warning; 116 | } 117 | td.score-40p.score { 118 | background-color: #FF9D19; 119 | } 120 | td.score-20p.score { 121 | background-color: #FF6D33; 122 | } 123 | td.danger.score, td.danger.date { 124 | background-color:$brand-danger; 125 | } 126 | .glyphicon { 127 | color:$gray-lighter; 128 | } 129 | a .glyphicon { 130 | color:$gray-dark; 131 | } 132 | } 133 | } 134 | 135 | .glyphicon.glyphicon-question-sign { 136 | cursor: help; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import 'isomorphic-fetch' 4 | import Promise from 'bluebird' 5 | import csv from 'csv' 6 | 7 | let csvParser = Promise.promisify(csv.parse) 8 | 9 | function getBackend() { 10 | var db = process.env.DATABASE_LOCATION || 11 | 'https://rawgit.com/okfn/data-quality-uk-25k-spend/master/data' 12 | var publisherTable = process.env.PUBLISHER_TABLE || 'publishers.csv' 13 | var sourceTable = process.env.SOURCE_TABLE || 'sources.csv' 14 | var resultTable = process.env.RESULT_TABLE || 'results.csv' 15 | var runTable = process.env.RUN_TABLE || 'runs.csv' 16 | var performanceTable = process.env.PERFORMANCE_TABLE || 'performance.csv' 17 | var instanceTable = process.env.INSTANCE_TABLE || 'instance.json' 18 | var showPricing = process.env.SHOW_PRICING_IN_MENU || false 19 | showPricing === 'true' ? showPricing = true : showPricing = false 20 | var pricingPageUrl = process.env.PRICING_PAGE_URL || '' 21 | if (showPricing && !pricingPageUrl) { 22 | throw Error('Please provide PRICING_PAGE_URL if you want to show pricing.'); 23 | } 24 | return { 25 | publishers: `${db}/${publisherTable}`, 26 | sources: `${db}/${sourceTable}`, 27 | results: `${db}/${resultTable}`, 28 | runs: `${db}/${runTable}`, 29 | performance: `${db}/${performanceTable}`, 30 | instance: `${db}/${instanceTable}`, 31 | pricingPageUrl: pricingPageUrl, 32 | showPricing: showPricing 33 | } 34 | } 35 | 36 | function getJSONEndpoint(endpoint) { 37 | return fetch(endpoint) 38 | .then(response => response.json()) 39 | .catch(console.trace.bind(console)) 40 | } 41 | 42 | function getCSVEndpoint(endpoint) { 43 | return fetch(endpoint) 44 | .then(response => response.text()) 45 | .then(text => csvParser(text, {columns: true})) 46 | .catch(console.trace.bind(console)) 47 | } 48 | 49 | export default { getBackend, getJSONEndpoint, getCSVEndpoint } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-quality-dashboard", 3 | "version": "0.1.0", 4 | "description": "A dashboard for tracking and displaying open data publication quality.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "mocha --require babel-core/register tests/", 9 | "build:front": "webpack --config webpack.config.development.js", 10 | "build:front:min": "webpack --config webpack.config.production.js", 11 | "build": "npm run build:front && npm run build:front:min", 12 | "prepublish": "npm run build", 13 | "postinstall": "npm run build", 14 | "review": "jscs lib test", 15 | "fix": "jscs lib test --fix", 16 | "watch": "webpack --watch --config webpack.config.production.js" 17 | }, 18 | "keywords": [ 19 | "open data", 20 | "open spending", 21 | "open knowledge", 22 | "data packages", 23 | "frictionless data" 24 | ], 25 | "dependencies": { 26 | "babel-core": "^6.1.2", 27 | "babel-polyfill": "^6.0.16", 28 | "babel-preset-es2015": "^6.1.2", 29 | "babel-preset-react": "^6.1.2", 30 | "bluebird": "2.9.24", 31 | "bootstrap": "^3.3.5", 32 | "bootstrap-sass": "3.4.1", 33 | "chart.js": "^1.0.2", 34 | "csv": "0.4.6", 35 | "express": "4.13.3", 36 | "express-react-views": "0.9.0", 37 | "history": "^1.12.5", 38 | "isomorphic-fetch": "^2.1.1", 39 | "jquery": "^3.4.1", 40 | "lodash": "4.17.15", 41 | "marked": "0.7.0", 42 | "moment": "^2.14.1", 43 | "nconf": "0.8.2", 44 | "node-cache": "3.2.1", 45 | "object-assign": "2.0.0", 46 | "react": "0.14.0", 47 | "react-bootstrap": "0.29.4", 48 | "react-bootstrap-async-autocomplete": "0.0.3", 49 | "react-chartjs": "0.6.0", 50 | "react-dom": "0.14.0", 51 | "react-redux": "3.1.0", 52 | "react-router": "^1.0.0-rc3", 53 | "react-tools": "0.13.3", 54 | "redux": "3.0.2", 55 | "redux-actions": "^0.8.0", 56 | "redux-logger": "2.0.4", 57 | "redux-router": "^1.0.0-beta3", 58 | "redux-thunk": "^1.0.0", 59 | "throng": "1.0.1" 60 | }, 61 | "devDependencies": { 62 | "babel-cli": "^6.8.0", 63 | "babel-core": "^6.8.0", 64 | "babel-loader": "^6.2.4", 65 | "babel-polyfill": "^6.8.0", 66 | "babel-preset-es2015": "^6.6.0", 67 | "babel-preset-react": "^6.5.0", 68 | "chai": "^3.4.0", 69 | "css-loader": "^0.23.1", 70 | "extract-text-webpack-plugin": "^1.0.1", 71 | "mocha": "^2.3.3", 72 | "node-sass": "^3.7.0", 73 | "redux-devtools": "^2.1.5", 74 | "sass-loader": "^3.2.0", 75 | "style-loader": "^0.13.1", 76 | "webpack": "^1.13.1", 77 | "zombie": "^4.2.1" 78 | }, 79 | "engines": { 80 | "node": "6.1.0", 81 | "npm": "3.8.6" 82 | }, 83 | "author": "Open Knowledge (https://okfn.org/)", 84 | "contributors": [ 85 | "Paul Walsh (http://pwalsh.me/)", 86 | "Tryggvi Bjorgvinsson ", 87 | "Helene Durand", 88 | "Dan Fowler (http://www.danfowler.net)", 89 | "Rufus Pollock (http://rufuspollock.org/)", 90 | "Georgiana Bere " 91 | ], 92 | "homepage": "https://github.com/okfn/data-quality-dashboard", 93 | "bugs": { 94 | "url": "https://github.com/okfn/data-quality-dashboard/issues" 95 | }, 96 | "repository": { 97 | "type": "git", 98 | "url": "git+https://github.com/okfn/data-quality-dashboard.git" 99 | }, 100 | "license": "AGPL-3.0", 101 | "directories": { 102 | "test": "tests" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /public/scripts/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/styles/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NOTE: We compile ES6 on the server at runtime, but not this module. 4 | require('babel-core/register'); 5 | 6 | var throng = require('throng'); 7 | var app = require('./app'); 8 | var WORKERS = process.env.WEB_CONCURRENCY || 1; 9 | 10 | // NOTE: we cache our data in memory on the app object, so if we start to 11 | // horizontally scale, we should also setup a service for the cache. 12 | throng(app.start, { 13 | workers: WORKERS, 14 | lifetime: Infinity 15 | }); 16 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Browser from 'zombie'; 3 | import {expect} from 'chai'; 4 | import start from '../app'; 5 | 6 | process.env.PORT = 3001; 7 | Browser.localhost('127.0.0.1', process.env.PORT); 8 | 9 | before(function(done) { 10 | start(done); 11 | }); 12 | 13 | describe('Can visit core routes', function() { 14 | var browser = new Browser({maxWait: 5000}); 15 | this.timeout(5000); 16 | it('should return the main page', function (done) { 17 | browser.visit('/', function() { 18 | browser.assert.success(); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './app/ui/scripts', 6 | devtool: 'source-map', 7 | module: { 8 | loaders: [ 9 | { test: /\.js$/, loaders: [ 'babel-loader' ], exclude: /node_modules/ }, 10 | { test: /\.html$/, loader: 'raw' }, 11 | { test: /\.scss$/, loader: ExtractTextPlugin.extract('css!sass') } 12 | ] 13 | }, 14 | output: { library: 'dq', libraryTarget: 'umd' } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var _ = require('lodash') 4 | var webpack = require('webpack') 5 | var baseConfig = require('./webpack.config.base') 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | 8 | var developmentConfig = { 9 | output: { 10 | filename: 'scripts/app.js', 11 | path: './public' 12 | }, 13 | plugins: [ 14 | new ExtractTextPlugin('styles/app.css'), 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': JSON.stringify('development') 18 | }) 19 | ] 20 | } 21 | 22 | var config = _.merge({}, baseConfig, developmentConfig) 23 | 24 | module.exports = config 25 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var _ = require('lodash') 4 | var webpack = require('webpack') 5 | var baseConfig = require('./webpack.config.base') 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | 8 | var productionConfig = { 9 | output: { 10 | filename: 'scripts/app.min.js', 11 | path: './public' 12 | }, 13 | plugins: [ 14 | new ExtractTextPlugin('styles/app.min.css'), 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': JSON.stringify('production') 18 | }), 19 | new webpack.optimize.UglifyJsPlugin({ 20 | compressor: { 21 | screw_ie8: true, 22 | warnings: false 23 | } 24 | }) 25 | ] 26 | } 27 | 28 | var config = _.merge({}, baseConfig, productionConfig) 29 | 30 | module.exports = config 31 | --------------------------------------------------------------------------------