mirror of
https://github.com/dolthub/dolt.git
synced 2026-05-13 03:10:03 -05:00
CrunchBase UI: Hook up the data to the backend
This changes the indexer to only index what we need since without chunked maps there is too much data to transfer and decode. The graph is also changed to a line graph since using a bar chart for this is not very intuitive.
This commit is contained in:
@@ -22,6 +22,12 @@ before_script:
|
||||
- npm run build
|
||||
- npm test
|
||||
- popd
|
||||
- pushd clients/crunchbase/ui
|
||||
- npm install
|
||||
- ./link.sh
|
||||
- npm run build
|
||||
- npm test
|
||||
- popd
|
||||
script:
|
||||
- export GODEBUG=invalidptr=0
|
||||
- go test ./...
|
||||
|
||||
@@ -68,13 +68,32 @@ func main() {
|
||||
c <- entry{qk, roundRaiseDef}
|
||||
}
|
||||
|
||||
// Compute a cutoff date which is later used to only include rounds after this date to reduce the amount of data.
|
||||
now := time.Now()
|
||||
currentYear := time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
lastQ := lastQuarter(now)
|
||||
var timeCutoff time.Time
|
||||
if currentYear.Before(lastQ) {
|
||||
timeCutoff = currentYear
|
||||
} else {
|
||||
timeCutoff = lastQ
|
||||
}
|
||||
|
||||
go func() {
|
||||
v.IterAllP(64, func(permalink string, r RefOfCompany) {
|
||||
company := r.TargetValue(ds)
|
||||
categoryList := company.CategoryList()
|
||||
regionKey := NewKey(ds).SetRegion(company.Region())
|
||||
// Skip region for now to reduce size of data.
|
||||
// regionKey := NewKey(ds).SetRegion(company.Region())
|
||||
|
||||
company.Rounds().IterAll(func(r RefOfRound) {
|
||||
round := r.TargetValue(ds)
|
||||
|
||||
// HACK: Only include rounds that are newer than the cutoff date.
|
||||
if time.Unix(round.FundedAt(), 0).Before(timeCutoff) {
|
||||
return
|
||||
}
|
||||
|
||||
roundRaiseDef := RoundRaiseDef{
|
||||
Raised: round.RaisedAmountUsd(),
|
||||
Details: r.TargetRef(),
|
||||
@@ -84,7 +103,9 @@ func main() {
|
||||
c <- entry{key, roundRaiseDef}
|
||||
})
|
||||
|
||||
c <- entry{regionKey, roundRaiseDef}
|
||||
// Skip region for now to reduce size of data.
|
||||
// c <- entry{regionKey, roundRaiseDef}
|
||||
|
||||
addTimeRounds(round.FundedAt(), roundRaiseDef)
|
||||
|
||||
roundType := classifyRoundType(round)
|
||||
@@ -99,7 +120,7 @@ func main() {
|
||||
for e := range c {
|
||||
key := e.key
|
||||
roundRaiseDef := e.roundRaiseDef
|
||||
keyRef := key.Ref()
|
||||
keyRef := types.WriteValue(key, ds)
|
||||
setDef := mapOfSets[keyRef]
|
||||
if setDef == nil {
|
||||
setDef = SetOfRoundRaiseDef{}
|
||||
@@ -164,3 +185,19 @@ func timeToQuarter(t time.Time) QuarterEnum {
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func lastQuarter(t time.Time) time.Time {
|
||||
var m time.Month
|
||||
switch t.Month() {
|
||||
case time.January, time.February, time.March:
|
||||
m = time.January
|
||||
case time.April, time.May, time.June:
|
||||
m = time.April
|
||||
case time.July, time.August, time.September:
|
||||
m = time.July
|
||||
case time.October, time.November, time.December:
|
||||
m = time.October
|
||||
}
|
||||
currentQuarter := time.Date(time.Now().Year(), m, 1, 0, 0, 0, 0, time.UTC)
|
||||
return currentQuarter.AddDate(0, -3, 0)
|
||||
}
|
||||
|
||||
@@ -19,5 +19,8 @@ func main() {
|
||||
if _, present := os.LookupEnv("NOMS_SERVER"); !present {
|
||||
os.Setenv("NOMS_SERVER", "http://localhost:8000")
|
||||
}
|
||||
if _, present := os.LookupEnv("NOMS_DATASET_ID"); !present {
|
||||
os.Setenv("NOMS_DATASET_ID", "crunchbase/index")
|
||||
}
|
||||
runner.ForceRun("npm", "run", "build")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<p>The nerdosphere dataset is imported montly from the <a href="http://data.crunchbase.com/v3/page/crunchbase-data-exports">CrunchBase Excel Data Export</a>, which is in turned based on crowdsourced data from <a href="http://www.crunchbase.com/">CrunchBase</a>.
|
||||
|
||||
<p>This graph shows the distribution of venture capital funding rounds by funding series. The x-axis is round size by decile, and the y-axis is the average round size within that decile.
|
||||
<p>This graph shows the distribution of venture capital funding rounds by funding series. The x-axis shows the rounds in increasing order. The y-axis is the amound of USD raised in that round.
|
||||
|
||||
<h2>Get the data</h2>
|
||||
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
pushd ../../../js
|
||||
npm install
|
||||
npm run build
|
||||
./link.sh
|
||||
popd
|
||||
npm link noms
|
||||
@@ -31,7 +31,7 @@
|
||||
"watchify": "^3.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cp node_modules/nvd3/build/nv.d3.css nv.d3.css; watchify -o out.js -v -d src/main.js",
|
||||
"start": "cp node_modules/nvd3/build/nv.d3.css nv.d3.css; NOMS_SERVER=http://localhost:8000 NOMS_DATASET_ID=crunchbase/index watchify -o out.js -v -d node_modules/babel-regenerator-runtime/runtime.js src/main.js",
|
||||
"build": "./.npm_build_helper.sh",
|
||||
"test": "rm -f out.js && eslint src/ && flow src/"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
export function invariant(exp: any, message: string = 'Invariant violated') {
|
||||
if (!exp) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
|
||||
import d3 from './d3.js';
|
||||
import nv from 'nvd3';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import type {DataArray} from './data.js';
|
||||
|
||||
type Props = {
|
||||
data: DataArray,
|
||||
color?: Array<string>
|
||||
};
|
||||
|
||||
type State = {
|
||||
chart: ?Object
|
||||
};
|
||||
|
||||
type DefaultProps = {};
|
||||
|
||||
export default class Chart extends React.Component<DefaultProps, Props, State> {
|
||||
componentDidMount() {
|
||||
nv.addGraph(() => {
|
||||
let chart = nv.models.lineChart();
|
||||
chart.options({
|
||||
clipEdge: true,
|
||||
color: this.props.color,
|
||||
interpolate: 'basis',
|
||||
isArea: true,
|
||||
showControls: false,
|
||||
showLegend: false,
|
||||
useInteractiveGuideline: true
|
||||
});
|
||||
|
||||
chart.yScale(d3.scale.log());
|
||||
let d3Format = d3.format('.2s');
|
||||
let format = v => '$' + d3Format(v).toUpperCase().replace('G', 'B');
|
||||
chart.yAxis.tickValues([1e4, 1e5, 1e6, 1e7, 1e8, 1e9]);
|
||||
chart.yAxis.tickFormat(format);
|
||||
chart.yDomain([1e3, 1e9]);
|
||||
chart.xAxis.tickFormat(v => (v * 100 | 0) + '%');
|
||||
|
||||
let div = ReactDOM.findDOMNode(this);
|
||||
d3.select(div.firstChild).datum(this.props.data).call(chart);
|
||||
nv.utils.windowResize(() => chart.update());
|
||||
this.setState({chart});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let chart = this.state.chart;
|
||||
if (chart) {
|
||||
let svg = ReactDOM.findDOMNode(this);
|
||||
d3.select(svg).datum(this.props.data).call(chart);
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.Element {
|
||||
return <svg/>;
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* @flow */
|
||||
// @flow
|
||||
|
||||
/* $FlowIssue - Flow fails to find the d3 module? */
|
||||
import d3 from 'd3';
|
||||
|
||||
@@ -1,33 +1,262 @@
|
||||
/* @flow */
|
||||
// @flow
|
||||
|
||||
export type DataPoint = {x: number, y: number};
|
||||
export type DataEntry = {values: Array<DataPoint>, key: string, color?: string};
|
||||
import {HttpStore, readValue, Struct, makeType, Ref, registerPackage} from 'noms';
|
||||
import {invariant} from './assert.js';
|
||||
import type {ChunkStore, Package} from 'noms';
|
||||
|
||||
type RoundTypeEnum = 0 | 1 | 2;
|
||||
const Seed = 0;
|
||||
const SeriesA = 1;
|
||||
const SeriesB = 2;
|
||||
|
||||
type QuarterEnum = 0 | 1 | 2 | 3;
|
||||
|
||||
type KeyParam = {
|
||||
Year: number,
|
||||
Quarter?: QuarterEnum
|
||||
} | {
|
||||
Category: string
|
||||
} | {
|
||||
RoundType: RoundTypeEnum
|
||||
};
|
||||
|
||||
type TimeOption = {
|
||||
Year: number,
|
||||
Quarter?: QuarterEnum
|
||||
};
|
||||
|
||||
type DataPoint = {x: number, y: number};
|
||||
type DataEntry = {values: Array<DataPoint>, key: string, color?: string};
|
||||
export type DataArray = Array<DataEntry>;
|
||||
|
||||
export default function getAllData() : DataArray {
|
||||
let seed = [];
|
||||
let seriesA = [];
|
||||
let seriesB = [];
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
seed.push({x: i, y: i});
|
||||
seriesA.push({x: i, y: i});
|
||||
seriesB.push({x: i, y: i});
|
||||
export default class DataManager {
|
||||
_store: HttpStore;
|
||||
_datasetId: string;
|
||||
_keyClass: any;
|
||||
_quarterClass: any;
|
||||
_index: Map<string, Ref>;
|
||||
_datasetP: ?Promise<Map<Ref, Ref>>;
|
||||
_packageP: ?Promise<Package>;
|
||||
|
||||
_categorySetP: ?Promise<Set<Struct>>;
|
||||
_timeSetP: ?Promise<Set<Struct>>;
|
||||
_seedSetP: ?Promise<Set<Struct>>;
|
||||
_seriesASetP: ?Promise<Set<Struct>>;
|
||||
_seriesBSetP: ?Promise<Set<Struct>>;
|
||||
|
||||
_data: ?DataArray;
|
||||
_time: ?TimeOption;
|
||||
_category: string;
|
||||
|
||||
constructor(store: ChunkStore, datasetId: string) {
|
||||
this._datasetId = datasetId;
|
||||
this._store = store;
|
||||
this._keyClass = null;
|
||||
this._quarterClass = null;
|
||||
this._datasetP = null;
|
||||
this._packageP = null;
|
||||
this._index = new Map();
|
||||
|
||||
this._timeSetP = null;
|
||||
this._categorySetP = null;
|
||||
this._seedSetP = null;
|
||||
this._seriesASetP = null;
|
||||
this._seriesBSetP = null;
|
||||
|
||||
this._data = null;
|
||||
this._time = null;
|
||||
this._category = '';
|
||||
}
|
||||
return [
|
||||
{
|
||||
values: seed,
|
||||
key: 'Seed',
|
||||
color: '#011f4b'
|
||||
},
|
||||
{
|
||||
values: seriesA,
|
||||
key: 'A',
|
||||
color: '#03396c'
|
||||
},
|
||||
{
|
||||
values: seriesB,
|
||||
key: 'B',
|
||||
color: '#005b96'
|
||||
|
||||
async _getDataset(): Promise<Map<Ref, Ref>> {
|
||||
if (this._datasetP) {
|
||||
return this._datasetP;
|
||||
}
|
||||
];
|
||||
return this._datasetP = getDataset(this._datasetId, this._store);
|
||||
}
|
||||
|
||||
async _getPackage(): Promise<Package> {
|
||||
if (this._packageP) {
|
||||
return this._packageP;
|
||||
}
|
||||
|
||||
let ds = await this._getDataset();
|
||||
this._packageP = getKeyPackage(ds, this._store);
|
||||
this._index = convertMap(ds);
|
||||
return this._packageP;
|
||||
}
|
||||
|
||||
async _getKeyClass(): Promise<any> {
|
||||
if (this._keyClass) return this._keyClass;
|
||||
let pkg = await this._getPackage();
|
||||
return this._keyClass = getStructClass(pkg, 'Key');
|
||||
}
|
||||
|
||||
async _getQuarterClass(): Promise<any> {
|
||||
if (this._quarterClass) return this._quarterClass;
|
||||
let pkg = await this._getPackage();
|
||||
return this._quarterClass = getStructClass(pkg, 'Quarter');
|
||||
}
|
||||
|
||||
_setTime(time: TimeOption) {
|
||||
let t = this._time;
|
||||
if (!t || t.Year !== time.Year || t.Quarter !== time.Quarter) {
|
||||
this._time = time;
|
||||
this._timeSetP = this._getSetOfRounds(time);
|
||||
this._data = null;
|
||||
}
|
||||
}
|
||||
|
||||
_setCategory(category: string) {
|
||||
if (this._category !== category) {
|
||||
this._category = category;
|
||||
this._categorySetP = this._getSetOfRounds({Category: category});
|
||||
this._data = null;
|
||||
}
|
||||
}
|
||||
|
||||
_createRounds() {
|
||||
this._seedSetP = this._getSetOfRounds({RoundType: Seed});
|
||||
this._seriesASetP = this._getSetOfRounds({RoundType: SeriesA});
|
||||
this._seriesBSetP = this._getSetOfRounds({RoundType: SeriesB});
|
||||
}
|
||||
|
||||
async getData(time: TimeOption, category: string): any {
|
||||
if (!this._seedSetP) {
|
||||
this._createRounds();
|
||||
}
|
||||
this._setTime(time);
|
||||
this._setCategory(category);
|
||||
|
||||
if (this._data) return this._data;
|
||||
|
||||
invariant(this._seedSetP && this._seriesASetP && this._seriesBSetP &&
|
||||
this._timeSetP && this._categorySetP);
|
||||
let [seedSet, seriesASet, seriesBSet, timeSet, categorySet] =
|
||||
await Promise.all([this._seedSetP, this._seriesASetP, this._seriesBSetP,
|
||||
this._timeSetP, this._categorySetP]);
|
||||
|
||||
let baseSet = intersectRounds(timeSet, categorySet);
|
||||
|
||||
return this._data = [
|
||||
{
|
||||
values: percentiles(intersectRounds(baseSet, seedSet)),
|
||||
key: 'Seed'
|
||||
},
|
||||
{
|
||||
values: percentiles(intersectRounds(baseSet, seriesASet)),
|
||||
key: 'A'
|
||||
},
|
||||
{
|
||||
values: percentiles(intersectRounds(baseSet, seriesBSet)),
|
||||
key: 'B'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async _getKeyRef(p: KeyParam): Promise<Ref> {
|
||||
const Key = await this._getKeyClass();
|
||||
let k: typeof Key;
|
||||
if (p.Quarter !== undefined) {
|
||||
let Quarter = await this._getQuarterClass();
|
||||
k = new Key({Quarter: new Quarter(p)});
|
||||
} else {
|
||||
k = new Key(p);
|
||||
}
|
||||
return k.ref;
|
||||
}
|
||||
|
||||
async _getSetOfRounds(p: KeyParam): Promise<Set<Struct>> {
|
||||
let s = (await this._getKeyRef(p)).toString();
|
||||
let v = this._index.get(s);
|
||||
if (v === undefined) {
|
||||
return new Set();
|
||||
}
|
||||
let set = readValue(v, this._store);
|
||||
return set || new Set();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the first key in the index and gets the package from the type.
|
||||
*/
|
||||
async function getKeyPackage(index: Map<Ref, Ref>, store: ChunkStore):
|
||||
Promise<Package> {
|
||||
let ref: Ref;
|
||||
for (let v of index.keys()) {
|
||||
ref = v;
|
||||
break;
|
||||
}
|
||||
invariant(ref instanceof Ref);
|
||||
let key = await readValue(ref, store);
|
||||
let pkg = await readValue(key.type.packageRef, store);
|
||||
registerPackage(pkg);
|
||||
return pkg;
|
||||
}
|
||||
|
||||
function getStructClass(pkg, name): typeof Struct {
|
||||
let keyIndex = pkg.types.findIndex(t => t.name === name);
|
||||
let type = makeType(pkg.ref, keyIndex);
|
||||
let typeDef = pkg.types[keyIndex];
|
||||
|
||||
return class extends Struct {
|
||||
constructor(data) {
|
||||
super(type, typeDef, data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getDataset(id: string, httpStore: ChunkStore): any {
|
||||
let rootRef = await httpStore.getRoot();
|
||||
let datasets = await readValue(rootRef, httpStore);
|
||||
let commitRef = datasets.get(id);
|
||||
let commit = await readValue(commitRef, httpStore);
|
||||
return commit.get('value');
|
||||
}
|
||||
|
||||
function convertMap<T>(map: Map<Ref, T>): Map<string, T> {
|
||||
let m = new Map();
|
||||
map.forEach((v, k) => {
|
||||
m.set(k.toString(), v);
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
function intersectRounds(a: Set<Struct>, b: Set<Struct>): Set<Struct> {
|
||||
let sa = new Set();
|
||||
a.forEach(v => {
|
||||
sa.add(v.ref.toString());
|
||||
});
|
||||
let s = new Set();
|
||||
b.forEach(v => {
|
||||
if (sa.has(v.ref.toString())) {
|
||||
s.add(v);
|
||||
}
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
function percentiles(s: Set<Struct>): Array<{x: number, y: number}> {
|
||||
let arr: Array<number> = [];
|
||||
for (let round of s) {
|
||||
let v = round.get('Raised');
|
||||
|
||||
if (v > 0) {
|
||||
arr.push(v);
|
||||
}
|
||||
|
||||
}
|
||||
arr.sort((a, b) => a - b);
|
||||
let len = arr.length;
|
||||
if (len === 0) {
|
||||
return [];
|
||||
}
|
||||
if (len === 1) {
|
||||
return [{x: 0, y: arr[0]}, {x: 1, y: arr[0]}];
|
||||
}
|
||||
return arr.map((y, i) => {
|
||||
let x = i / (len - 1);
|
||||
return {x, y};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props<T> = {
|
||||
item: T,
|
||||
delegate: LabelDelegate<T>
|
||||
};
|
||||
|
||||
export type LabelDelegate<T> = {
|
||||
getLabel: (item: T) => string,
|
||||
isSelected: (item: T) => boolean,
|
||||
getColor: (item: T) => string,
|
||||
onChange: (item: T) => void
|
||||
};
|
||||
|
||||
export default function Label(props: Props) : React.Element {
|
||||
let {delegate, item} = props;
|
||||
return <label><input type="checkbox"
|
||||
checked={delegate.isSelected(item)}
|
||||
onChange={() => delegate.onChange(item)}/>
|
||||
<span style={{color: delegate.getColor(item)}}/>
|
||||
<span>{delegate.getLabel(item)}</span>
|
||||
</label>;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Label from './label.js';
|
||||
import type {LabelDelegate} from './label.js';
|
||||
|
||||
type Props<T> = {
|
||||
delegate: LabelDelegate<T>,
|
||||
items: Array<T>,
|
||||
title?: string
|
||||
};
|
||||
|
||||
export default function LabelList<T>(props: Props<T>) : React.Element {
|
||||
let delegate = props.delegate;
|
||||
let labels = props.items.map(item => {
|
||||
return <Label key={delegate.getLabel(item)} item={item}
|
||||
delegate={delegate}/>;
|
||||
});
|
||||
return <div className='selection-list'>
|
||||
{props.title ? <h3>{props.title}</h3> : null}
|
||||
{labels}
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props<T> = {
|
||||
delegate: ListDelegate<T>,
|
||||
items: Array<T>,
|
||||
title?: string
|
||||
};
|
||||
|
||||
export default function List<T>(props: Props<T>) : React.Element {
|
||||
let delegate = props.delegate;
|
||||
let labels = props.items.map(item => {
|
||||
return <Label key={delegate.getLabel(item)} item={item}
|
||||
delegate={delegate}/>;
|
||||
});
|
||||
return <div className='selection-list'>
|
||||
{props.title ? <h3>{props.title}</h3> : null}
|
||||
{labels}
|
||||
</div>;
|
||||
}
|
||||
|
||||
type LabelProps<T> = {
|
||||
item: T,
|
||||
delegate: ListDelegate<T>
|
||||
};
|
||||
|
||||
export type ListDelegate<T> = {
|
||||
getLabel: (item: T) => string,
|
||||
isSelected: (item: T) => boolean,
|
||||
getColor?: (item: T) => string,
|
||||
onChange: (item: T) => void
|
||||
};
|
||||
|
||||
function Label(props: LabelProps) : React.Element {
|
||||
let {delegate, item} = props;
|
||||
return <label><input type="checkbox"
|
||||
checked={delegate.isSelected(item)}
|
||||
onChange={() => delegate.onChange(item)}/>
|
||||
<span style={{color: delegate.getColor && delegate.getColor(item)}}/>
|
||||
<span>{delegate.getLabel(item)}</span>
|
||||
</label>;
|
||||
}
|
||||
@@ -1,78 +1,182 @@
|
||||
/* @flow */
|
||||
// @flow
|
||||
|
||||
import getAllData from './data.js';
|
||||
import RadioList from './radio_list.js';
|
||||
import {HttpStore} from 'noms';
|
||||
import Chart from './chart.js';
|
||||
import DataManager from './data.js';
|
||||
import List from './list.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import SeriesList from './series_list.js';
|
||||
import StackedBarChart from './stacked_bar_chart.js';
|
||||
import type {DataArray, DataEntry} from './data.js';
|
||||
import type {DataArray} from './data.js';
|
||||
import type {ListDelegate} from './list.js';
|
||||
|
||||
type LabelAndKey = {
|
||||
label: string,
|
||||
key: Object
|
||||
};
|
||||
|
||||
type DefaultProps = {};
|
||||
|
||||
type Props = {
|
||||
data: DataArray,
|
||||
timeItems: Array<string>,
|
||||
categories: Array<string>
|
||||
series: Array<string>,
|
||||
timeItems: Array<LabelAndKey>,
|
||||
categories: Array<string>,
|
||||
color: Array<string>
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedSeries: Set<DataEntry>,
|
||||
selectedTimeItem: string,
|
||||
selectedCategoryItem: string
|
||||
selectedSeries: Set<string>,
|
||||
selectedTimeItem: LabelAndKey,
|
||||
selectedCategoryItem: string,
|
||||
data: DataArray
|
||||
};
|
||||
|
||||
const nomsServer: string = process.env.NOMS_SERVER;
|
||||
if (!nomsServer) {
|
||||
throw new Error('NOMS_SERVER not set');
|
||||
}
|
||||
const datasetId: string = process.env.NOMS_DATASET_ID;
|
||||
if (!datasetId) {
|
||||
throw new Error('NOMS_DATASET_ID not set');
|
||||
}
|
||||
|
||||
class Main extends React.Component<DefaultProps, Props, State> {
|
||||
_dataManager: DataManager;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
let selectedTimeItem = props.timeItems[0];
|
||||
let selectedCategoryItem = props.categories[0];
|
||||
|
||||
this._dataManager = new DataManager(new HttpStore(nomsServer), datasetId);
|
||||
|
||||
this.state = {
|
||||
selectedSeries: new Set(props.data),
|
||||
selectedTimeItem: props.timeItems[0],
|
||||
selectedCategoryItem: props.categories[0]
|
||||
selectedSeries: new Set(this.props.series),
|
||||
selectedTimeItem,
|
||||
selectedCategoryItem,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
_filteredData() : DataArray {
|
||||
return this.props.data.filter(o => this.state.selectedSeries.has(o));
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) : boolean {
|
||||
return nextProps !== this.props ||
|
||||
nextState.selectedSeries !== this.state.selectedSeries ||
|
||||
nextState.selectedTimeItem !== this.state.selectedTimeItem ||
|
||||
nextState.selectedCategoryItem !== this.state.selectedCategoryItem ||
|
||||
nextState.data !== this.state.data;
|
||||
}
|
||||
|
||||
_selectedSeriesChanged(s: Set<DataEntry>) {
|
||||
_filteredData(): DataArray {
|
||||
return this.state.data.filter(o => this.state.selectedSeries.has(o.key));
|
||||
}
|
||||
|
||||
_selectedSeriesChanged(s: Set<string>) {
|
||||
this.setState({selectedSeries: s});
|
||||
}
|
||||
|
||||
_selectedTimeChanged(s: string) {
|
||||
this.setState({selectedTimeItem: s});
|
||||
_selectedTimeChanged(item: LabelAndKey) {
|
||||
this.setState({selectedTimeItem: item});
|
||||
}
|
||||
|
||||
_selectedCategoryChanged(s: string) {
|
||||
this.setState({selectedCategoryItem: s});
|
||||
}
|
||||
|
||||
render() : React.Element {
|
||||
render(): React.Element {
|
||||
let s = this.state;
|
||||
let dm = this._dataManager;
|
||||
dm.getData(s.selectedTimeItem.key, s.selectedCategoryItem).then(data => {
|
||||
this.setState({data});
|
||||
}).catch(ex => {
|
||||
console.error(ex); // eslint-disable-line
|
||||
});
|
||||
|
||||
let seriesDelegate: ListDelegate<string> = {
|
||||
getLabel(item: string): string {
|
||||
return item;
|
||||
},
|
||||
isSelected: (item: string) => {
|
||||
return this.state.selectedSeries.has(item);
|
||||
},
|
||||
getColor: (item: string) => {
|
||||
return this.props.color[this.props.series.indexOf(item)];
|
||||
},
|
||||
onChange: (item: string) => {
|
||||
let selectedSeries = new Set(this.state.selectedSeries);
|
||||
if (selectedSeries.has(item)) {
|
||||
selectedSeries.delete(item);
|
||||
} else {
|
||||
selectedSeries.add(item);
|
||||
}
|
||||
this.setState({selectedSeries});
|
||||
}
|
||||
};
|
||||
|
||||
let timeDelegate: ListDelegate<LabelAndKey> = {
|
||||
getLabel(item: LabelAndKey): string {
|
||||
return item.label;
|
||||
},
|
||||
isSelected: (item: LabelAndKey) => {
|
||||
return item === this.state.selectedTimeItem;
|
||||
},
|
||||
onChange: (item: LabelAndKey) => {
|
||||
this._selectedTimeChanged(item);
|
||||
}
|
||||
};
|
||||
|
||||
let categoryDelegate: ListDelegate<string> = {
|
||||
getLabel(item: string): string {
|
||||
return item;
|
||||
},
|
||||
isSelected: (item: string) => {
|
||||
return item === this.state.selectedCategoryItem;
|
||||
},
|
||||
onChange: (item: string) => {
|
||||
this._selectedCategoryChanged(item);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className='app'>
|
||||
<div>
|
||||
<StackedBarChart className='app-chart' data={this._filteredData()}/>
|
||||
<Chart className='app-chart' data={this._filteredData()}
|
||||
color={this.props.color}/>
|
||||
<div className='app-controls'>
|
||||
<SeriesList title='Series' data={this.props.data}
|
||||
selected={this.state.selectedSeries}
|
||||
onChange={s => this._selectedSeriesChanged(s)}/>
|
||||
<RadioList title='Time' items={this.props.timeItems}
|
||||
selected={this.state.selectedTimeItem}
|
||||
onChange={s => this._selectedTimeChanged(s)}/>
|
||||
<RadioList title='Categories' items={this.props.categories}
|
||||
selected={this.state.selectedCategoryItem}
|
||||
onChange={s => this._selectedCategoryChanged(s)}/>
|
||||
<List title='Series' items={this.props.series}
|
||||
delegate={seriesDelegate}/>
|
||||
<List title='Time' items={this.props.timeItems}
|
||||
delegate={timeDelegate}/>
|
||||
<List title='Categories' items={this.props.categories}
|
||||
delegate={categoryDelegate}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const data = getAllData();
|
||||
const timeItems = ['Current Quarter', 'Current Year'];
|
||||
const categories = ['Finance', 'Tech', 'Bio', 'Education'];
|
||||
const series = ['Seed', 'A', 'B'];
|
||||
|
||||
let d = new Date();
|
||||
let year = d.getFullYear();
|
||||
d.setMonth(d.getMonth() - 3);
|
||||
let qYear = d.getFullYear();
|
||||
let quarter = (d.getMonth() + 1) / 4 | 0;
|
||||
const timeItems = [
|
||||
{label: 'Current Year', key: {Year: year}},
|
||||
{label: 'Last Quarter', key: {Year: qYear, Quarter: quarter}}
|
||||
];
|
||||
|
||||
const categories = [
|
||||
'Biotechnology',
|
||||
'Finance',
|
||||
'Games',
|
||||
'Software'
|
||||
];
|
||||
|
||||
const color = ['#011f4b', '#03396c', '#005b96'];
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
ReactDOM.render(
|
||||
<Main data={data} timeItems={timeItems} categories={categories}/>,
|
||||
<Main series={series} timeItems={timeItems} categories={categories}
|
||||
color={color}/>,
|
||||
document.querySelector('#app'));
|
||||
});
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import LabelList from './label_list.js';
|
||||
import type {LabelDelegate} from './label.js';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
items: Array<string>,
|
||||
selected: string,
|
||||
onChange: (selection: string) => void
|
||||
};
|
||||
|
||||
export default function RadioList(props: Props) : React.Element {
|
||||
let delegate: LabelDelegate<string> = {
|
||||
getLabel(item: string) : string {
|
||||
return item;
|
||||
},
|
||||
isSelected(item: string) : boolean {
|
||||
return item === props.selected;
|
||||
},
|
||||
getColor() : string {
|
||||
return '';
|
||||
},
|
||||
onChange(item: string) : void {
|
||||
props.onChange(item);
|
||||
}
|
||||
};
|
||||
|
||||
return <LabelList title={props.title} items={props.items} delegate={delegate}/>;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import type {DataArray} from './data.js';
|
||||
import LabelList from './label_list.js';
|
||||
import type {DataEntry} from './data.js';
|
||||
import type {LabelDelegate} from './label.js';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
data: DataArray,
|
||||
selected: Set<DataEntry>,
|
||||
onChange: (selection: Set<DataEntry>) => void
|
||||
};
|
||||
|
||||
export default function SeriesList(props: Props) : React.Element {
|
||||
let {selected} = props;
|
||||
let delegate: LabelDelegate<DataEntry> = {
|
||||
getLabel(item: DataEntry) : string {
|
||||
return item.key;
|
||||
},
|
||||
isSelected(item: DataEntry) : boolean {
|
||||
return selected.has(item);
|
||||
},
|
||||
getColor(item: DataEntry) : string {
|
||||
return item.color || '';
|
||||
},
|
||||
onChange(item: DataEntry) : void {
|
||||
selected = new Set(selected);
|
||||
selected.has(item) ? selected.delete(item) : selected.add(item);
|
||||
props.onChange(selected);
|
||||
}
|
||||
};
|
||||
|
||||
return <LabelList title={props.title} items={props.data} delegate={delegate}/>;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import d3 from './d3.js';
|
||||
import nv from 'nvd3';
|
||||
import React from 'react'; //eslint-disable-line no-unused-lets
|
||||
import ReactDOM from 'react-dom';
|
||||
import type {DataArray} from './data.js';
|
||||
|
||||
type Props = {
|
||||
data: DataArray
|
||||
};
|
||||
|
||||
type State = {
|
||||
chart: ?Object
|
||||
};
|
||||
|
||||
type DefaultProps = {};
|
||||
|
||||
export default class StackedBarChart extends React.Component<DefaultProps, Props, State> {
|
||||
componentDidMount() {
|
||||
nv.addGraph(() => {
|
||||
let chart = nv.models.multiBarChart();
|
||||
chart.options({
|
||||
stacked: true,
|
||||
showControls: false,
|
||||
showLegend: false
|
||||
});
|
||||
|
||||
let div = ReactDOM.findDOMNode(this);
|
||||
d3.select(div.firstChild).datum(this.props.data).call(chart);
|
||||
nv.utils.windowResize(() => chart.update());
|
||||
this.setState({chart});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let chart = this.state.chart;
|
||||
if (chart) {
|
||||
let svg = ReactDOM.findDOMNode(this);
|
||||
d3.select(svg).datum(this.props.data).call(chart);
|
||||
}
|
||||
}
|
||||
|
||||
render() : React.Element {
|
||||
return <svg/>;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: 'helvetica', sans-serif;
|
||||
font-family: helvetica, sans-serif;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ a:visited {
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
font: normal 12px Arial;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ a:visited {
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@@ -120,3 +120,7 @@ a:visited {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app .nvd3 text {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.*/node_modules/babylon/.*
|
||||
.*/node_modules/fbjs/.*
|
||||
.*/node_modules/react/.*
|
||||
.*/node_modules/d3/.*
|
||||
|
||||
[include]
|
||||
|
||||
@@ -11,3 +12,4 @@
|
||||
[options]
|
||||
unsafe.enable_getters_and_setters=true
|
||||
munge_underscores=true
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
||||
|
||||
+2
-1
@@ -348,7 +348,8 @@ class JsonArrayReader {
|
||||
|
||||
let s: { [key: string]: any } = Object.create(null);
|
||||
|
||||
for (let field of desc.fields) {
|
||||
for (let i = 0; i < desc.fields.length; i++) {
|
||||
let field = desc.fields[i];
|
||||
if (field.optional) {
|
||||
let b = this.readBool();
|
||||
if (b) {
|
||||
|
||||
+3
-2
@@ -201,7 +201,7 @@ class JsonArrayWriter {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Not implemented');
|
||||
throw new Error(`Not implemented: ${t.kind} ${v}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,8 @@ class JsonArrayWriter {
|
||||
writeStruct(s: Struct, type: Type, typeDef: Type, pkg: Package) {
|
||||
let desc = typeDef.desc;
|
||||
invariant(desc instanceof StructDesc);
|
||||
for (let field of desc.fields) {
|
||||
for (let i = 0; i < desc.fields.length; i++) {
|
||||
let field = desc.fields[i];
|
||||
let fieldValue = s.get(field.name);
|
||||
if (field.optional) {
|
||||
if (fieldValue !== undefined) {
|
||||
|
||||
+27
-22
@@ -1,26 +1,31 @@
|
||||
/* @flow */
|
||||
|
||||
import Chunk from './chunk.js';
|
||||
import type {ChunkStore} from './chunk_store.js';
|
||||
import CompoundList from './compound_list.js';
|
||||
import HttpStore from './http_store.js';
|
||||
import MemoryStore from './memory_store.js';
|
||||
import Ref from './ref.js';
|
||||
import Struct from './struct.js';
|
||||
import {encodeNomsValue} from './encode.js';
|
||||
import {readValue} from './decode.js';
|
||||
import {Type} from './type.js';
|
||||
// @flow
|
||||
|
||||
export {encodeNomsValue} from './encode.js';
|
||||
export {readValue} from './decode.js';
|
||||
export {default as Chunk} from './chunk.js';
|
||||
export {default as CompoundList} from './compound_list.js';
|
||||
export {default as HttpStore} from './http_store.js';
|
||||
export {default as MemoryStore} from './memory_store.js';
|
||||
export {default as Ref} from './ref.js';
|
||||
export {default as Struct} from './struct.js';
|
||||
export {lookupPackage, Package, readPackage, registerPackage} from './package.js';
|
||||
export {
|
||||
Chunk,
|
||||
CompoundList,
|
||||
encodeNomsValue,
|
||||
HttpStore,
|
||||
MemoryStore,
|
||||
readValue,
|
||||
Ref,
|
||||
Struct,
|
||||
Type
|
||||
};
|
||||
CompoundDesc,
|
||||
EnumDesc,
|
||||
Field,
|
||||
makeCompoundType,
|
||||
makeEnumType,
|
||||
makePrimitiveType,
|
||||
makeStructType,
|
||||
makeType,
|
||||
makeUnresolvedType,
|
||||
PrimitiveDesc,
|
||||
StructDesc,
|
||||
Type,
|
||||
typeType,
|
||||
packageType,
|
||||
UnresolvedDesc
|
||||
} from './type.js';
|
||||
|
||||
import type {ChunkStore} from './chunk_store.js';
|
||||
export type {ChunkStore};
|
||||
|
||||
+5
-3
@@ -107,17 +107,19 @@ function validate(s: Struct): ?Field {
|
||||
// TODO: Validate field values match field types.
|
||||
let data = s._data;
|
||||
let dataCount = Object.keys(data).length;
|
||||
s.desc.fields.forEach(field => {
|
||||
for (let i = 0; i < s.desc.fields.length; i++) {
|
||||
let field = s.desc.fields[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
dataCount--;
|
||||
} else {
|
||||
invariant(field.optional);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (s.desc.union.length > 0) {
|
||||
invariant(dataCount === 1);
|
||||
for (let field of s.desc.union) {
|
||||
for (let i = 0; i < s.desc.union.length; i++) {
|
||||
let field = s.desc.union[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
return field;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user