diff --git a/client/app/components/bar-chart.js b/client/app/components/bar-chart.js deleted file mode 100644 index 93c33b62c..000000000 --- a/client/app/components/bar-chart.js +++ /dev/null @@ -1,82 +0,0 @@ -/* global c3 */ -import Ember from 'ember'; - -export default Ember.Component.extend({ - - classNames: ['chart'], - - sourcesList: Ember.computed('data', function() { - return this.get('data').map(({ key, doc_count }) => [key, doc_count]).slice(0, 10); - }), - - dataChanged: Ember.observer('aggregations', function() { - this.updateBar(); - }), - - sizeChanged: Ember.observer('resizedSignal', function() { - this.updateBar(); - }), - - updateBar() { - this.set('data', this.get('aggregations.contributors.buckets')); - let columns = this.get('sourcesList'); // jscs:ignore - let title = 'Top 10 Contributors: '; - let bar = this.get('bar'); - if (bar) { - bar.load({ - columns, - unload: true - }); - bar.resize({ - height: this.get('height')*150-20, - width: this.get('width')*150 - }); - } else { - this.initBar(title, columns); - } - }, - - initBar(title, columns) { - let element = this.$(`.bar`).get(0); - let bar = c3.generate({ - bindto: element, - data: { - columns, - type: 'bar', - onclick: (d) => { - let url = 'https://share.osf.io/discover?q=' + d.name; - window.open(url, '_blank'); - } - }, - axis: { - x: { - tick: { - format: function() { - return 'Top 10 Contributors'; - } - } - }, - y: { - label: 'Number of Publications' - }, - }, - tooltip: { - grouped: false, // Default true - }, - legend: { show: false }, - bar: { - title, - label: { - show: false - } - }, - size: { height: this.get('height')*150-20 } - }); - this.set('bar', bar); - }, - - didRender() { - this.updateBar(); - }, - -}); diff --git a/client/app/components/donut-chart.js b/client/app/components/donut-chart.js deleted file mode 100644 index 3c1e2c10b..000000000 --- a/client/app/components/donut-chart.js +++ /dev/null @@ -1,64 +0,0 @@ -/* global c3 */ -import Ember from 'ember'; - -export default Ember.Component.extend({ - classNames: ['chart'], - - sourcesList: Ember.computed('data', function() { - return this.get('data').map(({ key, doc_count }) => [key, doc_count]); - }), - - dataChanged: Ember.observer('aggregations', function() { - this.updateDonut(); - }), - - data: [], - - sizeChanged: Ember.observer('resizedSignal', function() { - this.updateDonut(); - }), - - updateDonut() { - this.set('data', this.get('aggregations.sources.buckets')); - let columns = this.get('sourcesList'); - let title = 'Published in...'; - let donut = this.get('donut'); - if (donut) { - donut.load({ - columns, - unload: true - }); - donut.resize({ - height: this.get('height')*150-20, - width: this.get('width')*150 - }); - } else { - this.initDonut(title, columns); - } - }, - - initDonut(title, columns) { - let element = this.$(`.donut`).get(0); - let donut = c3.generate({ - bindto: element, - data: { - columns, - type: 'donut' - }, - legend: { show: false }, - donut: { - title, - label: { - show: false - } - }, - size: { height: this.get('height')*150-20 } - }); - this.set('donut', donut); - }, - - didRender() { - this.updateDonut(); - }, - -}); diff --git a/client/app/components/plotly-chart/component.js b/client/app/components/plotly-chart/component.js new file mode 100644 index 000000000..aa4e9f4c2 --- /dev/null +++ b/client/app/components/plotly-chart/component.js @@ -0,0 +1,190 @@ +/* global c3 */ +import Ember from 'ember'; + +export default Ember.Component.extend({ + + classNames: ['chart'], + charttitle: 'Untitled Chart', + + dataChanged: Ember.observer('aggregations', function() { + this.updateChart(); + }), + + data: [], + + sizeChanged: Ember.observer('resizedSignal', function() { + console.log("get resizedSignal"); + if (this.get('resizedSignal') === false) return; + this.updateChart(); + this.set('resizedSignal', false); + }), + + charTypeChange: Ember.observer('chartType', function(){ + this.updateChart(); + }), + + updateChart() { + + function log10ToLinear(log_num) { + if (log_num > 0) { + return 0; + } + return Math.pow(10, log_num-1).toFixed(0); + } + + function linearToLog10(lin_num) { + if (lin_num <= 0) { + return 0; + } + return (Math.log(lin_num) / Math.LN10) + 1; + } + + let chart_type = this.get('chartType'); + + let chart_options = { + bindto: this.element, + data: { + columns: null, //to be filled later + type: chart_type, + onclick: (d) => { + this.attrs.transitionToFacet(d); + }, + }, + legend: { show: false }, + [chart_type]: { + title: this.get('charttitle'), //to be filled later + label: { + show: false + } + }, + }; + + if (chart_type == 'donut') { + var title = ''; + var columns; + if (this.get('name') === 'Publishers') { + this.set('data', this.get('aggregations.publishers.buckets')); + columns = this.get('data').map(({ key, doc_count }) => [key, doc_count]); + } else if (this.get('name') === 'Events by Source'){ + this.set('data', this.get('aggregations.sources.buckets')); + columns = this.get('data').map(({ key, doc_count }) => [key, doc_count]); + } else { + this.set('data', this.get('aggregations.sources.buckets')); + columns = [['FIC', 685870], ['NCATS', 11798267], ['NCCIH', 527109], ['NCI', 37421432], ['NEI', 9258786], ['NHGRI', 5050824], ['NHLBI', 37973512], ['NIA', 30039371], ['NIAAA', 6709896], ['NIAID', 44746441], ['NIAMS', 6571101], ['NIBIB', 4005180], ['NICHD', 13278402], ['NIDA', 29823005], ['NIDCD', 4439991], ['NIDCR', 1155900], ['NIDDK', 32526462], ['NIEHS', 4674188], ['NIGMS', 53652460], ['NIMH', 38119017], ['NINDS', 20635337], ['NINR', 1976020], ['NLM', 1069055], ['OD', 4275564]]; + } + let labels = columns.map(function(value, index){ return value[0];}); + let values = columns.map(function(value, index){ return value[1];}); + var data = [{ + values: values, + labels: labels, + hoverinfo: 'label+percent', + hole: .6, + type: 'pie', + direction : 'clockwise', + textinfo: 'none', + }]; + + var layout = { + annotations:[{ + font: { + size: 12 + }, + showarrow: false, + x: 0.5, + y: 0.5, + text: '', + }], + paper_bgcolor : 'rgba(0,0,0,0)', + height: 320, + // autosize: true, + margin:{l: 25, r: 20, t: 20, b: 20, pad: 0}, + showlegend: false, + }; + + } else if (chart_type == 'bar') { + + this.set('data', this.get('aggregations.contributors.buckets')); + var columns = this.get('data').map(({ key, doc_count }) => [key, doc_count]).slice(0, 10); + + chart_options['axis'] = { + x: { + tick: { + format: function() { + return 'Top 10 Contributors'; + } + } + }, + y: { + label: 'Number of Publications' + }, + }; + chart_options['tooltip'] = { + grouped: false, // Default true + }; + chart_options['tooltip'] = tooltip; + + } else if (chart_type == 'relevanceHistogram') { + + let UC_hits = this.get('aggregations.filtered_score.buckets.institution.doc_count'); + let total_hits = this.get('total'); + + var columns = [ + ['overallCountByRelevance'].concat(this.get('data.aggregations.all_score.buckets').map((datum) => { + let val = this.get('aggregations.all_score.buckets')[datum.key]; + if (val && val.doc_count > 0) { return val.doc_count * UC_hits / total_hits; } + return 0; + })), + ['ucCountByRelevance'].concat(this.get('aggregations.all_score.buckets').map((datum) => { + let val = this.get('aggregations.filtered_score.buckets.institution.score.buckets')[datum.key]; + if (val && val.doc_count > 0) { return val.doc_count; } + return 0; + })), + ]; + + var data = [ + {y:columns[0].slice(1, -1), + name: 'overallCountByRelevance', + type: 'bar' + }, + {y:columns[1].slice(1, -1), + name: 'ucCountByRelevance', + type: 'bar' + } + ]; + var layout = {barmode: 'group', paper_bgcolor : 'rgba(0,0,0,0)', xaxis:{title: 'Relevnace Score'}, yaxis: {title: 'Number of Items', showline: true}}; + + } else if (chart_type == 'timeseries') { + let x_axis = this.get('data.aggregations.all_over_time.buckets').map((datum) => { return datum.key_as_string }) + var columns = this.get('data.aggregations.sorted_by_type.buckets').map((bucket) => { + return [bucket.key].concat(bucket['type_over_time'].buckets.reduce((ret, bucket) => { + ret[x_axis.indexOf(bucket.key_as_string)] = linearToLog10(bucket.doc_count); + return ret; + }, (new Array(x_axis.length)).fill(0))); + }); + columns.unshift(['x'].concat(x_axis)) + columns.unshift(['All Events'].concat(this.get('data.aggregations.all_over_time.buckets').map((bucket) => { + return linearToLog10(bucket.doc_count); + }))) + console.log(columns[0].length); + console.log(x_axis.length); + + var data = []; + for(var i = 0; i < columns.length; i++){ + data.push({x:x_axis, y: columns[i].slice(1, columns[i].length), fill: 'tozeroy', type: 'scatter', name: columns[i][0], line:{shape:'spline'}}); + } + + layout = {xaxis:{}, paper_bgcolor : 'rgba(0,0,0,0)', yaxis:{title: 'Number of Items (Log Scale)', tickformat: log10ToLinear, showline:true, showticklabels: true}}; + } + + Plotly.newPlot(this.element, data, layout, {displayModeBar: false}); + var graph = this.element; + window.addEventListener("resize", function(){ + Plotly.Plots.resize(graph); + }); + }, + + didRender() { + this.updateChart(); + }, + + }); diff --git a/client/app/components/plotly-chart/template.hbs b/client/app/components/plotly-chart/template.hbs new file mode 100644 index 000000000..b8de3efc2 --- /dev/null +++ b/client/app/components/plotly-chart/template.hbs @@ -0,0 +1,3 @@ +
diff --git a/client/app/components/timeseries-chart.js b/client/app/components/timeseries-chart.js deleted file mode 100644 index 38f7e5eed..000000000 --- a/client/app/components/timeseries-chart.js +++ /dev/null @@ -1,209 +0,0 @@ -/* global c3 */ -import Ember from 'ember'; - -export default Ember.Component.extend({ - - classNames: ['chart'], - - // Define boolean variables that specify which subsets (if any) of abstractcreativeworks we're specifically looking at - tPub: false, // Publication - tPre: false, // Preprint - tCre: false, // Creativework - tPro: false, // Project - - timeseriesList: Ember.computed('data', function() { // Format our timeseries data - let data = this.get('data'); - return [ - ['x'].concat(data.map((datum) => {return datum.key_as_string})), - ['Articles'].concat(data.map((datum) => {return datum.doc_count})), - ]; - }), - - data: [], - - dataChanged: Ember.observer('data', function() { // Initiate a chart update if the TS data changes - this.updateTS(); - }), - - sizeChanged: Ember.observer('resizedSignal', function() { - this.updateTS(); - }), - - updateTS() { // Update our TS chart when data/subsets change - this.set('data', this.get('aggregations.articles_over_time.buckets')); - let columns = this.get('timeseriesList'); - let title = ''; - let interval = this.get('interval'); - let ts = this.get('ts'); - if (ts) { - ts.load({ - columns, - unload: true - }); - ts.resize({ - height: this.get('height')*150-20, - width: this.get('width')*150 - }); - } else { - this.initTS(title, columns, interval); - } - }, - - pushTS(xCol) { // Add a specific subset of abstractcreativeworks to the TS chart - let ts = this.get('ts'); - ts.load({ - columns: [xCol] - }); - }, - - popTS(xCol) { // Remove a specific subset of abstractcreativeworks from the TS chart - let ts = this.get('ts'); - ts.unload([xCol]); - }, - - initTS(title, columns, interval) { // Draw the TS chart the first time its rendered - let element = this.$(`.ts`).get(0); - let ts = c3.generate({ - bindto: element, - data: { - x: 'x', - columns: columns, - types: { - x: 'area-spline', - Articles: 'area' - } - }, - axis: { - x: { - type: 'timeseries', - tick: { - culling: { - max: 10 - }, - rotate: 90, - format: '%d-%m-%Y' // Format the tick labels on our chart - } - } - }, - zoom: { - enabled: true - }, - tooltip: { // Format the tooltips on our chart - format: { // We want to return a nice-looking tooltip whose content is determined by (or at least consistent with) sour TS intervals - title: function (d) { - return d.toString().substring(4,15); // This isn't perfect, but it's at least more verbose than before - } - } - }, - point: { - show: false, - }, - size: { height: this.get('height')*150-20 } - }); - this.set('ts', ts); - }, - - init() { // Init should be used ONLY for setting component proprties. When we want to work on the component DOM element, we use didInsertElement hook - this._super(...arguments); - }, - - didRender() { // When this component has been inserted into the DOM - this.updateTS(); - }, - - // If the user wants to isolate preprints: - tPreChanged: Ember.observer('tPre', function() { - let otherSubsets = (this.get('tPub') || this.get('tCre') || this.get('tPro')); // check if we already are displaying article subsets on the chart - if(this.get('tPre')) { // if the user checked the box - this.filterTS('preprint',otherSubsets); - } - else { // if the user unchecked the box - if(!otherSubsets) { // if this is the only data on the chart right now and we're removing it - let data = this.get('timeseriesList'); // reload the original chart - this.updateTS(data); - } - else { - this.popTS('preprint'); - } - } - }), - - // If the user wants to isolate publications: - tPubChanged: Ember.observer('tPub', function() { - let otherSubsets = (this.get('tPre') || this.get('tCre') || this.get('tPro')); - if(this.get('tPub')) { // if the user checked the box - this.filterTS('publication',otherSubsets); - } - else { // if the user unchecked the box - if(!otherSubsets) { - let data = this.get('timeseriesList'); - this.updateTS(data); - } - else { - this.popTS('publication'); - } - } - }), - - // If the user wants to isolate creativeworks: - tCreChanged: Ember.observer('tCre', function() { - let otherSubsets = (this.get('tPre') || this.get('tPub') || this.get('tPro')); - if(this.get('tCre')) { // if the user checked the box - this.filterTS('creativework',otherSubsets); - } - else { // if the user unchecked the box - if(!otherSubsets) { - let data = this.get('timeseriesList'); - this.updateTS(data); - } - else { - this.popTS('creativework'); - } - } - }), - - // If the user wants to isolate projects: - tProChanged: Ember.observer('tPro', function() { - let otherSubsets = (this.get('tPre') || this.get('tPub') || this.get('tCre')); - if(this.get('tPro')) { // if the user checked the box - this.filterTS('project',otherSubsets); - } - else { // if the user unchecked the box - if(!otherSubsets) { - let data = this.get('timeseriesList'); - this.updateTS(data); - } - else { - this.popTS('project'); - } - } - }), - - // Isolate specific subsets of abstractcreativeworks - filterTS(typeString,o) { - let d = this.get('aggregations.articles_over_time.buckets'); - let firstRow = ['x']; - let secondRow = [typeString]; - d.forEach(function(entry) { - firstRow.push(entry.key_as_string); - let hasPubBucket = false; - entry.arttype.buckets.forEach(function(b) { - if(b.key === typeString) { - hasPubBucket = true; - secondRow.push(b.doc_count); - } - }); - if(!hasPubBucket) { - secondRow.push(0); - } - }); - if(o) { - this.pushTS(secondRow); - } - else { - let data = [firstRow, secondRow]; - this.updateTS(data); - } - } - -}); diff --git a/client/app/routes/dashboards/dashboard.js b/client/app/routes/dashboards/dashboard.js index 58eedd78a..4e7ce7742 100644 --- a/client/app/routes/dashboards/dashboard.js +++ b/client/app/routes/dashboards/dashboard.js @@ -28,7 +28,6 @@ export default Ember.Route.extend({ let interval = this.get('tsInterval'); let dashboards = { scholar: { - dashboardName: 'Scholar Dashboard', parameters: [ "scholar", @@ -659,11 +658,70 @@ export default Ember.Route.extend({ ] }, { - chartType: 'topContributors', - widgetType: 'list-widget', - name: 'Top Contributors', - width: 4, - post_body : { + chartType: 'relevanceHistogram', + widgetType: 'plotly-chart', + name:'Relevance Histogram', + width: 12, + facetDash: "arttype", + post_body: {}, + postBodyParams: [ + { + parameterName: "topic", + parameterPath: ["query", "bool", "must", 0, "query_string", "query"], + defaultValue: "*" + }, + { + parameterName: "institution", + parameterPath: ["aggregations", "filtered_score", "filters", "filters", "institution", "term", "sources.raw"] + }, + { + parameterName: "scoring", + parameterPath: ["aggregations", "filtered_score", "aggregations", "score"], + defaultValue: { + histogram: { + interval: 1, + script: { + lang: "expression", + inline: "_score * 10" + } + } + } + }, + { + parameterName: "scoring", + parameterPath: ["aggregations", "all_score"], + defaultValue: { + histogram: { + interval: 1, + script: { + lang: "expression", + inline: "_score * 10" + } + } + } + }, + { + parameterName: "scholar", + parameterPath: ["query", "bool", "filter", 2, "term", "contributors.raw"], + } + ] + }, + { + "chartType": 'topContributors', + "widgetType": 'list-widget', + "name": 'Top Tags', + "width": 4, + "post_body" : { + "query": { + "bool": { + "must": { + "query_string": { + "query": query + } + } + } + }, + "from": 0, "aggregations": { "listWidgetData": { "terms": { @@ -728,11 +786,52 @@ export default Ember.Route.extend({ facetDash: "institution", }, { - chartType: 'topContributors', - widgetType: 'list-widget', - name: 'Top Tags', + chartType: 'donut', + widgetType: 'plotly-chart', + name: 'Events by Source', width: 4, post_body: { + query: { + bool: { must: [{ + query_string: {query: query} + },{ + range: { date: { + gte: gte, + lte: lte, + format: "yyyy-MM-dd||yyyy" + } + } + } + ]} + }, + from: 0, + aggregations: { + sources: { + terms: { + field: 'sources.raw', + size: 200 + } + }, + } + }, + postBodyParams: [ + ], + facetDash: "institution", + }, + { + "chartType": 'topContributors', + "widgetType": 'list-widget', + "name": 'Top Contributors', + "width": 4, + "post_body" : { + "query": { + "bool": { + "must": { + "query_string": {"query": query} + } + } + }, + "from": 0, "aggregations": { "listWidgetData" : { "terms": { @@ -823,6 +922,67 @@ export default Ember.Route.extend({ parameterPath: ["query", "bool", "filter", 2, "term", "contributors.raw"], } ] + }, + { + chartType: 'timeseries', + widgetType: 'plotly-chart', + name:'Data Histogram', + width: 12, + post_body: { + "aggregations": { + "sorted_by_type": { + "terms": { + "field": "type" + }, + "aggregations": { + "type_over_time": { + "date_histogram": { + "field": "date_updated", + "interval": "1M", + "format": "yyyy-MM-dd" + } + } + } + }, + "all_over_time": { + "date_histogram": { + "field": "date_updated", + "interval": "1M", + "format": "yyyy-MM-dd" + } + } + } + }, + facetDash: "shareresults", + postBodyParams: [ + { + parameterName: "query", + parameterPath: ["query", "bool", "must", 0, "query_string", "query"], + defaultValue: "*" + }, + { + parameterPath: ["query", "bool", "must", 1, "range", "date", "lt"], + parameterName: "tillDate", + defaultValue: "now/d" + }, + { + parameterPath: ["query", "bool", "must", 1, "range", "date", "gte"], + parameterName: "fromDate", + defaultValue: "now-10y/d" + }, + { + parameterName: "topic", + parameterPath: ["query", "bool", "filter", 0, "term", "tags"] + }, + { + parameterName: "institution", + parameterPath: ["query", "bool", "filter", 1, "term", "sources.raw"], + }, + { + parameterName: "scholar", + parameterPath: ["query", "bool", "filter", 2, "term", "contributors.raw"], + } + ] } ] } diff --git a/client/bower.json b/client/bower.json index cfe53bc6e..6293d0869 100644 --- a/client/bower.json +++ b/client/bower.json @@ -14,6 +14,7 @@ "packery": "^2.1.1", "font-awesome": "^4.7.0", "toastr": "^2.1.3", - "freewall": "^1.0.6" + "freewall": "^1.0.6", + "plotlyjs": "plotly#^1.18.1" } } diff --git a/client/ember-cli-build.js b/client/ember-cli-build.js index 89f82456b..548e29e4f 100644 --- a/client/ember-cli-build.js +++ b/client/ember-cli-build.js @@ -26,6 +26,7 @@ module.exports = function(defaults) { app.import('bower_components/d3/d3.js'); app.import('bower_components/c3/c3.js'); + app.import('bower_components/plotlyjs/plotly.js'); app.import('bower_components/freewall/freewall.js'); app.import('bower_components/packery/dist/packery.pkgd.min.js'); app.import('bower_components/draggabilly/dist/draggabilly.pkgd.min.js') diff --git a/service/dashboard/migrations/0001_initial.py b/service/dashboard/migrations/0001_initial.py new file mode 100644 index 000000000..58bde90ea --- /dev/null +++ b/service/dashboard/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-05 20:36 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('widget', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Dashboard', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Unnamed Dashboard', max_length=64)), + ('settings', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('widgets', models.ManyToManyField(related_name='containing_dashboards', to='widget.Widget')), + ], + ), + ] diff --git a/service/service/settings/local.py b/service/service/settings/local.py deleted file mode 100644 index 4a06dfd9a..000000000 --- a/service/service/settings/local.py +++ /dev/null @@ -1,10 +0,0 @@ -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'public', - 'USER': 'admin', - 'PASSWORD': 'itsover9000', - 'HOST': '127.0.0.1', - 'PORT': '5432', - } -} diff --git a/service/widget/migrations/0001_initial.py b/service/widget/migrations/0001_initial.py new file mode 100644 index 000000000..8c3230592 --- /dev/null +++ b/service/widget/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-05 20:36 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Widget', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Unnamed Widget', max_length=32)), + ('author', models.TextField(max_length=20, null=True)), + ('width', models.IntegerField(default=2)), + ('height', models.IntegerField(default=2)), + ('query', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('settings', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ], + ), + ]