diff --git a/README.md b/README.md index 22937fb..456af74 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An interactive data visualization tool. VizWit uses a [JSON config file](https:/ interactive charts that cross-filter one another. It currently supports data hosted in a Socrata Open Data portal, which includes cities such as [Philadelphia](http://opendataphilly.org), [Chicago](https://data.cityofchicago.org/), [San Francisco](https://data.sfgov.org/) and many others. However, interactions with Socrata have been [abstracted](src/scripts/collections/socrata.js) to allow -for other data providers to be written (in theory). +for [other data providers](https://github.com/timwis/vizwit/wiki/Adding-a-provider) to be written. [![screencast](http://i.imgur.com/4gTXNFK.gif)](http://vizw.it/?gist=51db593dc0537d1a3f05) @@ -60,16 +60,17 @@ layout is generated by Gridstack. The application is compiled using Browserify. # How it works [vizwit.js](src/scripts/vizwit.js) is the primary chart-generating module. It takes a `container` selector and a `config` object and uses them to initialize a view (bar chart, map, table, etc.). The views are passed a `collection` and a `filteredCollection`, which are -identical and allow the view to query the data provider (Socrata). Views generate the chart/map/table/etc. and listen for user interactions +identical and allow the view to query the data provider. Views generate the chart/map/table/etc. and listen for user interactions on them. On an interaction, the view triggers the global event object with the filter (ie. `state=PA`), and all views that use the same dataset receive the event and use their `filteredCollection` to query the data provider with the filter passed through the event. By using a separate collection, VizWit can show the "filtered value" and the "original value" side-by-side. -The actual entry point is either [layout.js](src/scripts/layout.js) or [embed.js](src/scripts/embed.js). layout.js fetches a gist, reads -its configuration, and creates a layout on the page. Then for each chart in the configuration, it calls `init()` from vizwit.js as -described above. embed.js simply finds the parent container of the ` + + \ No newline at end of file diff --git a/src/embed-demo.html b/src/embed-demo.html index c1d317d..6bede83 100644 --- a/src/embed-demo.html +++ b/src/embed-demo.html @@ -13,7 +13,6 @@ - @@ -74,6 +73,6 @@

Hello, world!

- + \ No newline at end of file diff --git a/src/index.html b/src/index.html index 1c0a6b7..6ac3f5b 100644 --- a/src/index.html +++ b/src/index.html @@ -13,7 +13,6 @@ - @@ -42,6 +41,6 @@
- + \ No newline at end of file diff --git a/src/scripts/config/providers.js b/src/scripts/config/providers.js new file mode 100644 index 0000000..30c1967 --- /dev/null +++ b/src/scripts/config/providers.js @@ -0,0 +1,3 @@ +module.exports = { + socrata: require('../collections/socrata') +} diff --git a/src/scripts/layout.js b/src/scripts/layout.js index 9d9330b..6db5e95 100644 --- a/src/scripts/layout.js +++ b/src/scripts/layout.js @@ -1,88 +1,67 @@ -/* global global */ -var $ = global.jQuery = require('jquery') -var _ = global._ = require('underscore') +var $ = require('jquery') +var _ = require('underscore') var Backbone = require('backbone') -var deparam = require('node-jquery-deparam') -require('gridstack/dist/gridstack') var Header = require('./views/header') var vizwit = require('./vizwit') -var Gist = require('./collections/gist') var vent = _.clone(Backbone.Events) var fieldsCache = {} -var params = window.location.search.substr(1) ? deparam(window.location.search.substr(1)) : {} +module.exports = function (config, options) { + options = options || {} + if (!config.version || config.version !== '2') console.error('Wrong config version') -// If no gist specified, redirect to homepage -var redirect = function () { window.location.replace('http://vizwit.io') } -if (!params.gist) { - redirect() -} - -// Fetch gist -(new Gist(null, {id: params.gist})).fetch({ - success: function (collection, response, options) { - if (!collection.length) return console.error('No files in gist', params.gist) + // Render header + if (config.header) { + var header = new Header(config.header) + $(options.headerSelector).empty().append(header.render().el) - // If a file was provided, use that one; otherwise use the first file in the gist - var model = params.file && collection.get(params.file) ? collection.get(params.file) : collection.at(0) - var config = JSON.parse(model.get('content')) + // Update tag + if (config.header.title) { + var originalTitle = $('title').text() + $('title').text(config.header.title + ' - ' + originalTitle) + } + } - if (!config.version || config.version !== '2') return redirect() + var container = $(options.contentSelector) + var heightInterval = 60 // from gridstack.js + var current = {x: null, y: null} + var row - // Render header - if (config.header) { - var header = new Header(config.header) - $('#page-header').append(header.render().el) + container.empty() - // Update <title> tag - if (config.header.title) { - var originalTitle = $('title').text() - $('title').text(config.header.title + ' - ' + originalTitle) - } + config.cards.forEach(function (config) { + // If y suggests we're on a new row (including the first item), create a new row + if (config.y !== current.y) { + row = $('<div class="row"></div>') + container.append(row) + current.y = config.y + current.x = 0 } - var container = $('#page-content') - var heightInterval = 60 // from gridstack.js - var current = {x: null, y: null} - var row - - config.cards.forEach(function (config) { - // If y suggests we're on a new row (including the first item), create a new row - if (config.y !== current.y) { - row = $('<div class="row"></div>') - container.append(row) - current.y = config.y - current.x = 0 - } + var column = $('<div/>') - var column = $('<div/>') + // Add width class + column.addClass('col-sm-' + config.width) - // Add width class - column.addClass('col-sm-' + config.width) - - // If x is not the same as our current x position, add offset class - if (config.x !== current.x) { - column.addClass('col-sm-offset-' + (config.x - current.x)) - } - // Set height of new div - column.css('min-height', config.height * heightInterval) + // If x is not the same as our current x position, add offset class + if (config.x !== current.x) { + column.addClass('col-sm-offset-' + (config.x - current.x)) + } + // Set height of new div + column.css('min-height', config.height * heightInterval) - // Increment current.x to new starting position - current.x += config.width + // Increment current.x to new starting position + current.x += config.width - // Add the div to the current row - row.append(column) + // Add the div to the current row + row.append(column) - // Initialize vizwit on new div - vizwit.init(column, config.vizwit, { - vent: vent, - fieldsCache: fieldsCache - }) + // Initialize vizwit on new div + vizwit.init(column, config.vizwit, { + vent: vent, + fieldsCache: fieldsCache }) - }, - error: function () { - console.error('Error fetching gist', params.gist) - } -}) + }) +} diff --git a/src/scripts/providers.js b/src/scripts/providers.js deleted file mode 100644 index caab467..0000000 --- a/src/scripts/providers.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - socrata: require('./collections/socrata') -} diff --git a/src/scripts/vizwit-editor.js b/src/scripts/vizwit-editor.js new file mode 100644 index 0000000..726f40c --- /dev/null +++ b/src/scripts/vizwit-editor.js @@ -0,0 +1,28 @@ +var _ = require('underscore') +var split = require('split.js') +var ace = require('brace') +;require('brace/mode/json') +var layout = require('./layout') + +var sampleData = require('../data/sample.json') + +var layoutOptions = { + headerSelector: '#page-header', + contentSelector: '#page-content' +} + +split(['#editor', '#preview'], {sizes: [40, 60]}) + +var editor = ace.edit('editor') +var session = editor.getSession() +session.setMode('ace/mode/json') +editor.setValue(JSON.stringify(sampleData, null, 2), -1) + +var refresh = function () { + var input = JSON.parse(editor.getValue()) + layout(input, layoutOptions) +} + +session.on('change', _.debounce(refresh, 300)) + +refresh() diff --git a/src/scripts/embed.js b/src/scripts/vizwit-embed.js similarity index 100% rename from src/scripts/embed.js rename to src/scripts/vizwit-embed.js diff --git a/src/scripts/vizwit-loader.js b/src/scripts/vizwit-loader.js new file mode 100644 index 0000000..c6135dd --- /dev/null +++ b/src/scripts/vizwit-loader.js @@ -0,0 +1,40 @@ +var $ = require('jquery') +var deparam = require('node-jquery-deparam') +var Gist = require('./collections/gist') +var layout = require('./layout') + +var params = window.location.search.substr(1) ? deparam(window.location.search.substr(1)) : {} +var pathToFiles = 'data/' // should include trailing slash + +var layoutOptions = { + headerSelector: '#page-header', + contentSelector: '#page-content' +} + +// If gist ID specified, fetch it using github's API +if (params.gist) { + (new Gist(null, {id: params.gist})).fetch({ + success: function (collection, response, options) { + if (!collection.length) return console.error('No files in gist', params.gist) + + // If a file was provided, use that one; otherwise use the first file in the gist + var model = params.file && collection.get(params.file) ? collection.get(params.file) : collection.at(0) + var config = JSON.parse(model.get('content')) + + layout(config, layoutOptions) + }, + error: function () { + console.error('Error fetching gist', params.gist) + } + }) +// If file specified, load it from the files directory +} else if (params.file) { + $.getJSON(pathToFiles + params.file, function (data) { + layout(data, layoutOptions) + }).fail(function () { + console.error('Error loading file', params.file) + }) +// If no config specified, redirect to homepage +} else { + window.location.replace('http://vizwit.io') +} diff --git a/src/scripts/vizwit.js b/src/scripts/vizwit.js index 4cee661..84a62d7 100644 --- a/src/scripts/vizwit.js +++ b/src/scripts/vizwit.js @@ -2,7 +2,7 @@ var _ = require('underscore') var Backbone = require('backbone') -var Providers = require('./providers') +var Providers = require('./config/providers') var GeoJSON = require('./collections/geojson') var Bar = require('./views/bar') diff --git a/src/styles/editor.css b/src/styles/editor.css new file mode 100644 index 0000000..80c0063 --- /dev/null +++ b/src/styles/editor.css @@ -0,0 +1,37 @@ +html, body, .row { + height: 100%; +} + +body { + box-sizing: border-box; + margin: 0; +} + +.gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; +} + +.gutter.gutter-horizontal { + background-image: url('https://raw.githubusercontent.com/nathancahill/Split.js/master/grips/vertical.png'); + cursor: ew-resize; +} + +.gutter.gutter-vertical { + background-image: url('https://raw.githubusercontent.com/nathancahill/Split.js/master/grips/horizontal.png'); + cursor: ns-resize; +} + +.split { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; +} + +.split, .gutter.gutter-horizontal { + height: 100%; + float: left; +} \ No newline at end of file diff --git a/test/spec/collections/provider.js b/test/spec/collections/provider.js index 5e9a1bd..5731479 100644 --- a/test/spec/collections/provider.js +++ b/test/spec/collections/provider.js @@ -1,6 +1,6 @@ var test = require('tape') var BaseFields = require('../../../src/scripts/collections/basefields') -var providers = require('../../../src/scripts/providers') +var providers = require('../../../src/scripts/config/providers') var sampleFieldsData = require('../../fixtures/fields.json')