Skip to content

Commit

Permalink
Annual journal analysis viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
onejgordon committed Dec 30, 2018
1 parent 8f73bc8 commit 0666514
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 18 deletions.
16 changes: 15 additions & 1 deletion api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

from datetime import datetime, timedelta, time
from datetime import datetime, timedelta, time, date
from models import Project, Habit, HabitDay, Goal, MiniJournal, User, Task, \
Readable, TrackingDay, Event, JournalTag, Report, Quote, Snapshot
from constants import READABLE, GOAL
Expand Down Expand Up @@ -723,6 +723,20 @@ def list(self, d):
'journals': [j.json() for j in journals if j]
}, success=True)

@authorized.role('user')
def year(self, d):
year = self.request.get_range('year')
journal_keys = []
cursor = date(year, 1, 1)
for i in range(365):
iso_date = tools.iso_date(cursor)
journal_keys.append(ndb.Key('MiniJournal', iso_date, parent=self.user.key))
cursor += timedelta(days=1)
journals = ndb.get_multi(journal_keys)
self.set_response({
'journals': [j.json() for j in journals if j]
}, success=True)

@authorized.role('user')
def today(self, d):
'''
Expand Down
1 change: 1 addition & 0 deletions flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
webapp2.Route('/api/event/batch', handler=api.EventAPI, handler_method="batch_create", methods=["POST"]),
webapp2.Route('/api/event/delete', handler=api.EventAPI, handler_method="delete", methods=["POST"]),
webapp2.Route('/api/journal/today', handler=api.JournalAPI, handler_method="today", methods=["GET"]),
webapp2.Route('/api/journal/year', handler=api.JournalAPI, handler_method="year", methods=["GET"]),
webapp2.Route('/api/journal/submit', handler=api.JournalAPI, handler_method="submit", methods=["POST"]),
webapp2.Route('/api/journal', handler=api.JournalAPI, handler_method="list", methods=["GET"]),
webapp2.Route('/api/journal', handler=api.JournalAPI, handler_method="update", methods=["POST"]),
Expand Down
3 changes: 1 addition & 2 deletions src/js/components/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ var util = require('utils/util');
var api = require('utils/api');
var UserStore = require('stores/UserStore');
var UserActions = require('actions/UserActions');
var SimpleAdmin = require('components/common/SimpleAdmin');
var AsyncActionButton = require('components/common/AsyncActionButton');
var ReactJsonEditor = require('components/common/ReactJsonEditor');
import Select from 'react-select';
import {findItemById} from 'utils/store-utils';
import {RaisedButton, TextField, DatePicker, FontIcon,
import {TextField, DatePicker, FontIcon,
Paper, List, ListItem} from 'material-ui';
import {changeHandler} from 'utils/component-utils';
import {get, set, clone} from 'lodash';
Expand Down
11 changes: 2 additions & 9 deletions src/js/components/analysis/AnalysisGoals.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ var React = require('react');
import PropTypes from 'prop-types';
import {Bar} from "react-chartjs-2";
import {Dialog, IconButton} from 'material-ui';
import Select from 'react-select'
import connectToStores from 'alt-utils/lib/connectToStores';
var ProgressLine = require('components/common/ProgressLine');
var YearSelector = require('components/common/YearSelector');
var util = require('utils/util');
var api = require('utils/api');
import {changeHandler} from 'utils/component-utils';
Expand Down Expand Up @@ -157,12 +156,6 @@ export default class AnalysisGoals extends React.Component {
let content;
if (Object.keys(goals).length == 0) content = <div className="empty">No goal assessments yet</div>
else content = <Bar data={goalData} options={goalOptions} width={1000} height={450}/>
let year_cursor = this.FIRST_GOAL_YEAR;
let year_opts = []
while (year_cursor <= today.getFullYear()) {
year_opts.push({value: year_cursor, label: year_cursor})
year_cursor += 1;
}
return (
<div>

Expand All @@ -171,7 +164,7 @@ export default class AnalysisGoals extends React.Component {
<div className="row">
<div className="col-sm-3 col-sm-offset-9">
<label>Year</label>
<Select options={year_opts} value={form.year} onChange={this.changeHandlerVal.bind(this, 'form', 'year')} simpleValue clearable={false} />
<YearSelector first_year={this.FIRST_GOAL_YEAR} year={form.year} onChange={this.changeHandlerVal.bind(this, 'form', 'year')} />
<IconButton iconClassName="material-icons" onClick={this.fetch_goal_year.bind(this)}>refresh</IconButton>
</div>
</div>
Expand Down
159 changes: 154 additions & 5 deletions src/js/components/analysis/AnalysisJournals.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
var React = require('react');
var util = require('utils/util');
import {FlatButton, AutoComplete,
import {AutoComplete, FlatButton,
Checkbox, DropDownMenu, MenuItem} from 'material-ui';
var AppConstants = require('constants/AppConstants');
var YearSelector = require('components/common/YearSelector');
import PropTypes from 'prop-types';
import {Bar, Line} from "react-chartjs-2";
var api = require('utils/api');
import {get} from 'lodash';
import {GOOGLE_API_KEY} from 'constants/client_secrets';
var EntityMap = require('components/common/EntityMap');
import Select from 'react-select'
var ReactTooltip = require('react-tooltip');
import loadGoogleMapsAPI from 'load-google-maps-api';
import connectToStores from 'alt-utils/lib/connectToStores';
import {changeHandler} from 'utils/component-utils';

@connectToStores
@changeHandler
export default class AnalysisJournals extends React.Component {
static propTypes = {
journals: PropTypes.array
journals: PropTypes.array,
annual_viewer_journals: PropTypes.array
}

static defaultProps = {
Expand All @@ -34,18 +41,24 @@ export default class AnalysisJournals extends React.Component {
});
this.state = {
tags: [],
form: {},
tags_loading: false,
journal_tag_segment: null,
journal_segments: {}, // tag.id -> { data: [], labels: [] }
questions: questions,
color_scale_question: chartable.length > 0 ? chartable[0] : null,
chart_enabled_questions: chart_enabled,
map_showing: false,
google_maps: null // Holder for Google Maps object
google_maps: null, // Holder for Google Maps object
annual_viewer_journals: [],
data_ranges: {}
};

this.TAG_COLOR = '#3FE0F2'
this.TAG_BG_COLOR = `rgba(0, .4, 1, 0.3)`
this.ANNUAL_VIEWER_COLOR_RANGE = ["CC0000", "00FF00"]
this.MONTH_LETTERS = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
this.fetch_annual_journals = this.fetch_annual_journals.bind(this)
}

static getStores() {
Expand All @@ -60,6 +73,12 @@ export default class AnalysisJournals extends React.Component {

}

componentDidUpdate(prevProps, prevState) {
let {form} = this.state
let annual_key_change = form.annual_viewer_key != prevState.form.annual_viewer_key
if (annual_key_change) ReactTooltip.rebuild()
}

toggle_series(series) {
let {chart_enabled_questions} = this.state;
util.toggleInList(chart_enabled_questions, series);
Expand All @@ -77,6 +96,13 @@ export default class AnalysisJournals extends React.Component {
});
}

numeric_questions() {
let {questions} = this.state;
return questions.filter((q) => {
return AppConstants.NUMERIC_QUESTION_TYPES.indexOf(q.response_type) > -1;
});
}

journal_data() {
let {chart_enabled_questions, questions, journal_tag_segment} = this.state;
let {journals} = this.props;
Expand Down Expand Up @@ -237,8 +263,127 @@ export default class AnalysisJournals extends React.Component {
});
}

fetch_annual_journals() {
let {form} = this.state
let open_ended_numeric = this.numeric_questions().filter((q) => q.response_type == 'number_oe')
if (!form.annual_viewer_key) {
let qs = this.numeric_questions()
if (qs.length > 0) {
form.annual_viewer_key = qs[0].name
}
}
let today = new Date()
let year = form.annual_viewer_year || today.getFullYear()
api.get("/api/journal/year", {year: year}, (res) => {
let data_ranges = {}
this.numeric_questions().forEach((q) => {
let min, max
if (q.response_type == 'number_oe') {
// Open range, need to find max and min from fetched data
let values = res.journals.map((jrnl) => jrnl.data[q.name])
min = Math.min(values)
max = Math.max(values)
} else {
// Fixed range
min = 1
max = 10
}
data_ranges[q.name] = {
min: min,
max: max,
reverse: q.value_reverse
}
})
this.setState({annual_viewer_journals: res.journals, form: form, data_ranges: data_ranges}, () => {
ReactTooltip.rebuild()
})
})
}

render_annual_viewer() {
let {form, annual_viewer_journals, data_ranges} = this.state
let options = this.numeric_questions().map((q) => {
return {value: q.name, label: q.text}
})
let yr = form.annual_viewer_year || (new Date().getFullYear())
let jan_1 = util.date_from_iso(`${yr}-01-01`)
let cursor = jan_1
let grid = []
let last_month = null
let have_data = annual_viewer_journals.length > 0
let key = form.annual_viewer_key
if (have_data) {
let idx = 0
let min_val = data_ranges[key].min
let max_val = data_ranges[key].max
while (cursor.getFullYear() == yr) {
if (cursor.getMonth() != last_month) {
// New month
grid.push(<span className="monthLabel">{ this.MONTH_LETTERS[cursor.getMonth()] }</span>)
last_month = cursor.getMonth()
}
let jrnl = annual_viewer_journals[idx]
let have_date = jrnl != null && jrnl.iso_date == util.iso_from_date(cursor)
let st = {}
let iso_date = util.iso_from_date(cursor)
let tip = iso_date
if (have_date) {
let val = key == null ? 0 : jrnl.data[key]
let low_value_idx = data_ranges[key].reverse ? 1 : 0
let hi_value_idx = data_ranges[key].reverse ? 0 : 1
st.backgroundColor = '#' + util.colorInterpolate({
color1: this.ANNUAL_VIEWER_COLOR_RANGE[low_value_idx],
color2: this.ANNUAL_VIEWER_COLOR_RANGE[hi_value_idx],
min: min_val,
max: max_val,
value: val
})
idx += 1
tip += `: ${val}`
} else {
st.opacity = 0.5
}
grid.push(<span key={iso_date} className="square" data-tip={tip} style={st}></span>)
cursor.setDate(cursor.getDate() + 1)
}
}
let title = "Annual Viewer"
if (have_data) title += ` (${yr})`
return (
<div className="JournalAnnualView">
<h5 className="sectionBreak">{title}</h5>
<div className="row">
<div className="col-sm-4">
<YearSelector first_year={2016} year={form.annual_viewer_year} onChange={this.changeHandlerVal.bind(this, 'form', 'annual_viewer_year')} />
</div>
<div className="col-sm-4">
<FlatButton onClick={this.fetch_annual_journals} label="Load Journal Data" />
</div>
</div>
<div className="row">
<div className="col-sm-4" hidden={!have_data}>
<label>Results for Question</label>
<Select options={options}
name="annual_viewer_key"
onChange={this.changeHandlerVal.bind(this, 'form', 'annual_viewer_key')}
value={form.annual_viewer_key}
clearable={false}
simpleValue />
</div>
</div>

<div className="row">
<div className="grid">
{ grid }
</div>
</div>

</div>
)
}

render() {
let {journal_tag_segment, journal_segments, map_showing } = this.state;
let {journal_tag_segment, journal_segments, map_showing } = this.state
let {loaded} = this.props;
if (!loaded) return null;
let _journals_segmented
Expand Down Expand Up @@ -278,7 +423,7 @@ export default class AnalysisJournals extends React.Component {
};
_journals_segmented = (
<div>
<h4>Journals Segmented by {journal_tag_segment.id}</h4>
<h4 className="sectionBreak">Journals Segmented by {journal_tag_segment.id}</h4>

<Bar options={segmented_opts} data={{
labels: segmented_data.labels,
Expand Down Expand Up @@ -332,6 +477,8 @@ export default class AnalysisJournals extends React.Component {

<Line data={journalData} options={journalOptions} width={1000} height={450}/>

<h5 className="sectionBreak">Journals by Tag</h5>

<AutoComplete
hintText="Start typing #tag or @mention..."
dataSource={this.state.tags}
Expand All @@ -345,6 +492,8 @@ export default class AnalysisJournals extends React.Component {

{ _journals_segmented }

{ this.render_annual_viewer() }

</div>
);
}
Expand Down
44 changes: 44 additions & 0 deletions src/js/components/common/YearSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
var PropTypes = require('prop-types');
var React = require('react');
import Select from 'react-select'

export default class YearSelector extends React.Component {
static propTypes = {
year: PropTypes.number,
// One of following required
years_back: PropTypes.number,
first_year: PropTypes.number
}
static defaultProps = {
year: null,
years_back: 3
}

constructor(props) {
super(props);
this.state = {
}
}

handleChange(val) {
this.props.onChange(val)
}

render() {
let {year, years_back, first_year} = this.props
let today = new Date()
let today_year = today.getFullYear()
if (year == null) year = today_year
let year_cursor = first_year != null ? first_year : (today_year - years_back)
let year_opts = []
while (year_cursor <= today_year) {
year_opts.push({value: year_cursor, label: year_cursor})
year_cursor += 1
}
return <Select options={year_opts}
value={year}
onChange={this.handleChange.bind(this)}
simpleValue
clearable={false} />
}
}
2 changes: 2 additions & 0 deletions src/js/constants/AppConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ var AppConstants = {
USER_STORAGE_KEY: 'flowUser',
DEFAULT_WEEK_START: 0, // Sunday (d.getDay())
HABIT_ACTIVE_LIMIT: 20,
// Journals
JOURNAL_START_HOUR: 21, // Default
JOURNAL_END_HOUR: 4, // Default
NUMERIC_QUESTION_TYPES: ['slider', 'number', 'number_oe'],
GOAL_DEFAULT_SLOTS: 4,
GOAL_MAX_SLOTS: 10,
USER_ADMIN: 2,
Expand Down
Loading

0 comments on commit 0666514

Please sign in to comment.