Initial Timeline Explorer

This commit adds a TimelineExplorer for visualizing timeline cred data.
The centerpiece is the TimelineCredChart, a d3-based line chart showing
how the top users' cred evolved over time. It has features like tooltips,
reasonable ticks on the x axis, a legend, and filtering out line
segments that stay on the x axis.

An inspection test is included, which you can check out here:
http://localhost:8080/test/TimelineCredView/

Also, you can run it for any loaded repository at:
http://localhost:8080/timeline/$repoOwner/$repoName

This commit also includes new dependencies:
- recharts (for the charts)
- react-markdown (for rendering the Markdown descriptions)
- remove-markdown (so the legend will be clean text)
- d3-time-format for date axis generation
- d3-scale and d3-scale-chromatic for color scales

Test plan: The frontend code is mostly untested, in keeping with my
observation that the costs of testing the old explorer were really high,
and the tests brought little benefit. However, I have manually tested it
thoroughly. Also, there is an inspection test for the TimelineCredView
(see above).
This commit is contained in:
Dandelion Mané 2019-05-31 20:17:27 +03:00
parent b106326e0a
commit 5dc7f440ce
10 changed files with 819 additions and 4 deletions

View File

@ -8,7 +8,11 @@
"chalk": "2.4.2",
"commonmark": "^0.29.0",
"d3-array": "^2.2.0",
"d3-format": "^1.3.2",
"d3-scale": "^3.0.0",
"d3-scale-chromatic": "^1.3.3",
"d3-time": "^1.0.11",
"d3-time-format": "^2.1.3",
"express": "^4.16.3",
"fs-extra": "8.1.0",
"history": "^3.0.0",
@ -26,6 +30,8 @@
"react-icons": "^3.7.0",
"react-markdown": "^4.0.8",
"react-router": "3.2.1",
"recharts": "^1.6.2",
"remove-markdown": "^0.3.0",
"retry": "^0.12.0",
"rimraf": "^2.6.3",
"svg-react-loader": "^0.4.6",

104
src/explorer/TimelineApp.js Normal file
View File

@ -0,0 +1,104 @@
// @flow
import React from "react";
import type {Assets} from "../webutil/assets";
import type {RepoId} from "../core/repoId";
import {TimelineExplorer} from "./TimelineExplorer";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
import {DEFAULT_CRED_CONFIG} from "../plugins/defaultCredConfig";
export type Props = {|
+assets: Assets,
+repoId: RepoId,
+loader: Loader,
|};
export type Loader = (assets: Assets, repoId: RepoId) => Promise<LoadResult>;
export type LoadResult = Loading | LoadSuccess | LoadError;
export type Loading = {|+type: "LOADING"|};
export type LoadSuccess = {|
+type: "SUCCESS",
+timelineCred: TimelineCred,
|};
export type LoadError = {|+type: "ERROR", +error: any|};
export type State = {|
loadResult: LoadResult,
|};
export class TimelineApp extends React.Component<Props, State> {
state = {loadResult: {type: "LOADING"}};
componentDidMount() {
this.load();
}
async load() {
const loadResult = await this.props.loader(
this.props.assets,
this.props.repoId
);
this.setState({loadResult});
}
render() {
const {loadResult} = this.state;
switch (loadResult.type) {
case "LOADING": {
return (
<div style={{width: 900, margin: "0 auto"}}>
<h1>Loading...</h1>
</div>
);
}
case "ERROR": {
const {error} = loadResult;
return (
<div style={{width: 900, margin: "0 auto"}}>
<h1>Load Error:</h1>
<p>
{error.status}:{error.statusText}
</p>
</div>
);
}
case "SUCCESS": {
const {timelineCred} = loadResult;
return (
<TimelineExplorer
initialTimelineCred={timelineCred}
repoId={this.props.repoId}
declarations={[githubDeclaration]}
/>
);
}
default:
throw new Error(`Unexpected load state: ${(loadResult.type: empty)}`);
}
}
}
export async function defaultLoader(
assets: Assets,
repoId: RepoId
): Promise<LoadResult> {
async function fetchCred(): Promise<TimelineCred> {
const url = assets.resolve(
`api/v1/data/data/${repoId.owner}/${repoId.name}/cred.json`
);
const response = await fetch(url);
if (!response.ok) {
return Promise.reject(response);
}
return TimelineCred.fromJSON(await response.json(), DEFAULT_CRED_CONFIG);
}
try {
const timelineCred = await fetchCred();
return {type: "SUCCESS", timelineCred};
} catch (e) {
console.error(e);
return {type: "ERROR", error: e};
}
}

View File

@ -0,0 +1,126 @@
// @flow
import React from "react";
import removeMd from "remove-markdown";
import {schemeCategory10} from "d3-scale-chromatic";
import {timeFormat} from "d3-time-format";
import {scaleOrdinal} from "d3-scale";
import {timeMonth, timeYear} from "d3-time";
import {format} from "d3-format";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
import * as NullUtil from "../util/null";
import {type NodeAddressT} from "../core/graph";
import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred";
export type Props = {
timelineCred: TimelineCred,
displayedNodes: $ReadOnlyArray<NodeAddressT>,
};
type LineChartDatum = {
interval: Interval,
// May be null if the node score was filtered out (effectively bc.
// that node did not exist yet)
score: Map<NodeAddressT, ?number>,
};
// TODO: Make the line chart auto-scale based on its container
export const LINE_CHART_WIDTH = 1000;
export const LINE_CHART_HEIGHT = 500;
/**
* Renders a line chart showing cred over time for the node addresses in
* props.displayedNodes.
*
* To see a demo, you can check out the TimelineCredView inspection test.
* Run `yarn start` and navigate to:
* http://localhost:8080/test/TimelineCredView/
*/
export class TimelineCredChart extends React.Component<Props> {
render() {
const {timelineCred, displayedNodes} = this.props;
const intervals = timelineCred.intervals();
const timeDomain = [
intervals[0].startTimeMs,
intervals[intervals.length - 1].endTimeMs,
];
const data: LineChartDatum[] = intervals.map((interval, index) => {
const score = new Map();
for (const node of displayedNodes) {
const {cred} = NullUtil.get(timelineCred.credNode(node));
const lastScore = index === 0 ? 0 : cred[index - 1];
const nextScore = index === intervals.length - 1 ? 0 : cred[index + 1];
const thisScore = cred[index];
// Filter a score out if it's on the zero line and not going anywhere.
// This makes the tooltips a lot cleaner.
// (Ideally we would have more control over the tooltips display directly
// without munging the data...)
const filteredScore =
Math.max(lastScore, nextScore, thisScore) < 0.1 ? null : thisScore;
score.set(node, filteredScore);
}
return {score, interval};
});
const scale = scaleOrdinal(displayedNodes, schemeCategory10);
const Lines = displayedNodes.map((n: NodeAddressT) => {
const description = NullUtil.get(timelineCred.graph().node(n))
.description;
const plainDescription = removeMd(description);
return (
<Line
type="monotone"
dot={false}
key={n}
stroke={scale(n)}
dataKey={(x) => x.score.get(n)}
name={plainDescription}
/>
);
});
const formatMonth = timeFormat("%b");
const formatYear = timeFormat("%Y");
function multiFormat(dateMs) {
const date = new Date(dateMs);
return timeYear(date) < date ? formatMonth(date) : formatYear(date);
}
const ticks = timeMonth.range(...timeDomain);
return (
<LineChart
width={LINE_CHART_WIDTH}
height={LINE_CHART_HEIGHT}
data={data}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey={(x) => x.interval.startTimeMs}
type="number"
domain={timeDomain}
tickFormatter={multiFormat}
ticks={ticks}
/>
<YAxis />
{Lines}
<Tooltip
formatter={format(".1d")}
itemSorter={(x) => -x.value}
labelFormatter={(v) => {
return `Week of ${timeFormat("%B %e, %Y")(v)}`;
}}
/>
<Legend />
</LineChart>
);
}
}

View File

@ -0,0 +1,78 @@
// @flow
import React from "react";
import Markdown from "react-markdown";
import {sum} from "d3-array";
import {type NodeAddressT} from "../core/graph";
import {TimelineCredChart} from "./TimelineCredChart";
import {format} from "d3-format";
import {TimelineCred} from "../analysis/timeline/timelineCred";
export type Props = {|
+timelineCred: TimelineCred,
+selectedNodeFilter: NodeAddressT,
|};
const MAX_ENTRIES_PER_LIST = 100;
const DEFAULT_ENTRIES_PER_CHART = 6;
/**
* Render a view on TimelineCred.
*
* Takes a TimelineCred instance and a node filter as props. It will display
* cred for nodes that match the filter.
*
* The top `DEFAULT_ENTRIES_PER_CHART` nodes (by total cred across time) will
* be rendered in a TimelineCredChart. There is also a table showing the top
* `MAX_ENTRIES_PER_LIST` nodes (also by total cred across time).
*
* For a demo, check out TimelineCredViewInspectionTest by running `yarn start`
* and then navigating to:
* http://localhost:8080/test/TimelineCredView/
*/
export class TimelineCredView extends React.Component<Props> {
render() {
const {selectedNodeFilter, timelineCred} = this.props;
const nodes = timelineCred.credSortedNodes(selectedNodeFilter);
const tableNodes = nodes.slice(0, MAX_ENTRIES_PER_LIST);
const chartNodes = nodes
.slice(0, DEFAULT_ENTRIES_PER_CHART)
.map((x) => x.node.address);
const totalScore = sum(nodes.map((x) => x.total));
return (
<div style={{width: 1000, margin: "0 auto"}}>
<TimelineCredChart
timelineCred={timelineCred}
displayedNodes={chartNodes}
/>
<table style={{width: 600, margin: "0 auto", padding: "20px 10px"}}>
<thead>
<tr>
<th>Contributor</th>
<th style={{textAlign: "right"}}>Cred</th>
<th style={{textAlign: "right"}}>% Total</th>
</tr>
</thead>
<tbody>
{tableNodes.map(({node, total}) => {
return (
<tr key={node.address}>
<td>
<Markdown
renderers={{paragraph: "span"}}
source={node.description}
/>
</td>
<td style={{textAlign: "right"}}>{format(".1d")(total)}</td>
<td style={{textAlign: "right"}}>
{format(".1%")(total / totalScore)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}

View File

@ -0,0 +1,73 @@
// @flow
import React from "react";
import {timeWeek} from "d3-time";
import type {Assets} from "../webutil/assets";
import {TimelineCredView} from "./TimelineCredView";
import {Graph, NodeAddress} from "../core/graph";
import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred";
import {type FilteredTimelineCred} from "../analysis/timeline/filterTimelineCred";
import {defaultWeights} from "../analysis/weights";
import {DEFAULT_CRED_CONFIG} from "../plugins/defaultCredConfig";
export default class TimelineCredViewInspectiontest extends React.Component<{|
+assets: Assets,
|}> {
intervals(): Interval[] {
const startTimeMs = +new Date(2017, 0);
const endTimeMs = +new Date(2017, 6);
const boundaries = timeWeek.range(startTimeMs, endTimeMs);
const result = [];
for (let i = 0; i < boundaries.length - 1; i++) {
result.push({
startTimeMs: +boundaries[i],
endTimeMs: +boundaries[i + 1],
});
}
return result;
}
timelineCred(): TimelineCred {
const intervals = this.intervals();
const users = [
["starter", (x) => Math.max(0, 20 - x)],
["steady", (_) => 4],
["finisher", (x) => (x * x) / 20],
["latecomer", (x) => Math.max(0, x - 20)],
];
const graph = new Graph();
const addressToCred = new Map();
for (const [name, generator] of users) {
const address = NodeAddress.fromParts(["foo", name]);
graph.addNode({
address,
description: `[@${name}](https://github.com/${name})`,
timestampMs: null,
});
const scores = intervals.map((_unuesd, i) => generator(i));
addressToCred.set(address, scores);
}
const filteredTimelineCred: FilteredTimelineCred = {
intervals,
addressToCred,
};
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()};
return new TimelineCred(
graph,
filteredTimelineCred,
params,
DEFAULT_CRED_CONFIG
);
}
render() {
const selectedNodeFilter = NodeAddress.fromParts(["foo"]);
return (
<TimelineCredView
timelineCred={this.timelineCred()}
selectedNodeFilter={selectedNodeFilter}
/>
);
}
}

View File

@ -0,0 +1,160 @@
// @flow
import React from "react";
import deepEqual from "lodash.isequal";
import {type RepoId} from "../core/repoId";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {type Weights, copy as weightsCopy} from "../analysis/weights";
import {
TimelineCred,
type TimelineCredParameters,
} from "../analysis/timeline/timelineCred";
import {TimelineCredView} from "./TimelineCredView";
import {WeightConfig} from "./weights/WeightConfig";
import {WeightsFileManager} from "./weights/WeightsFileManager";
export type Props = {
repoId: RepoId,
initialTimelineCred: TimelineCred,
// TODO: Get this info from the TimelineCred
declarations: $ReadOnlyArray<PluginDeclaration>,
};
export type State = {
timelineCred: TimelineCred,
weights: Weights,
alpha: number,
intervalDecay: number,
loading: boolean,
showWeightConfig: boolean,
};
/**
* TimelineExplorer allows displaying, exploring, and re-calculating TimelineCred.
*
* It basically wraps a TimelineCredView with some additional features and options:
* - allows changing the weights and re-calculating cred with new weights
* - allows saving/loading weights
* - displays the RepoId
*/
export class TimelineExplorer extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const timelineCred = props.initialTimelineCred;
const {alpha, intervalDecay, weights} = timelineCred.params();
this.state = {
selectedNodeTypePrefix: timelineCred.config().scoreNodePrefix,
timelineCred,
alpha,
intervalDecay,
// Set the weights to a copy, to ensure we don't mutate the weights in the
// initialTimelineCred. This enables e.g. disabling the analyze button
// when the parameters are unchanged.
weights: weightsCopy(weights),
loading: false,
showWeightConfig: false,
};
}
params(): TimelineCredParameters {
const {alpha, intervalDecay, weights} = this.state;
// Set the weights to a copy, to ensure that the weights we pass into e.g.
// analyzeCred are a distinct reference from the one we keep in our state.
return {alpha, intervalDecay, weights: weightsCopy(weights)};
}
async analyzeCred() {
this.setState({loading: true});
const timelineCred = await this.state.timelineCred.reanalyze(this.params());
this.setState({timelineCred, loading: false});
}
renderConfigurationRow() {
const {showWeightConfig} = this.state;
const weightFileManager = (
<WeightsFileManager
weights={this.state.weights}
onWeightsChange={(weights: Weights) => {
this.setState({weights});
}}
/>
);
const weightConfig = (
<WeightConfig
declarations={this.props.declarations}
nodeTypeWeights={this.state.weights.nodeTypeWeights}
edgeTypeWeights={this.state.weights.edgeTypeWeights}
onNodeWeightChange={(prefix, weight) => {
this.setState(({weights}) => {
weights.nodeTypeWeights.set(prefix, weight);
return {weights};
});
}}
onEdgeWeightChange={(prefix, weight) => {
this.setState(({weights}) => {
weights.edgeTypeWeights.set(prefix, weight);
return {weights};
});
}}
/>
);
const paramsUpToDate = deepEqual(
this.params(),
this.state.timelineCred.params()
);
const analyzeButton = (
<button
disabled={this.state.loading || paramsUpToDate}
onClick={() => this.analyzeCred()}
>
re-compute cred
</button>
);
const {owner, name} = this.props.repoId;
return (
<div>
<div style={{marginTop: 30, display: "flex"}}>
<span style={{paddingLeft: 30}}>
cred for {owner}/{name}
<a href={`/prototype/${owner}/${name}/`}>(legacy)</a>
</span>
<span style={{flexGrow: 1}} />
<button
onClick={() => {
this.setState(({showWeightConfig}) => ({
showWeightConfig: !showWeightConfig,
}));
}}
>
{showWeightConfig
? "Hide weight configuration"
: "Show weight configuration"}
</button>
{analyzeButton}
</div>
{showWeightConfig && (
<div style={{marginTop: 10}}>
<span>Upload/Download weights:</span>
{weightFileManager}
{weightConfig}
</div>
)}
</div>
);
}
render() {
const timelineCredView = (
<TimelineCredView
timelineCred={this.state.timelineCred}
selectedNodeFilter={this.state.timelineCred.config().scoreNodePrefix}
/>
);
return (
<div style={{width: 900, margin: "0 auto"}}>
{this.renderConfigurationRow()}
{timelineCredView}
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// @flow
import React, {type ComponentType} from "react";
import type {RepoId} from "../core/repoId";
import type {Assets} from "../webutil/assets";
import HomepageTimeline from "./homepageTimeline";
export default function makeTimelinePage(
repoId: RepoId
): ComponentType<{|+assets: Assets|}> {
return class TimelinePage extends React.Component<{|+assets: Assets|}> {
render() {
return <HomepageTimeline assets={this.props.assets} repoId={repoId} />;
}
};
}

View File

@ -0,0 +1,22 @@
// @flow
import React from "react";
import type {Assets} from "../webutil/assets";
import {TimelineApp, defaultLoader} from "../explorer/TimelineApp";
import type {RepoId} from "../core/repoId";
export default class TimelineExplorer extends React.Component<{|
+assets: Assets,
+repoId: RepoId,
|}> {
render() {
return (
<TimelineApp
assets={this.props.assets}
repoId={this.props.repoId}
loader={defaultLoader}
/>
);
}
}

View File

@ -85,6 +85,15 @@ function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
title: `${entry.repoId.owner}/${entry.repoId.name} • SourceCred`,
navTitle: null,
})),
...registry.map((entry) => ({
path: `/timeline/${entry.repoId.owner}/${entry.repoId.name}/`,
contents: {
type: "PAGE",
component: () => require("./TimelinePage").default(entry.repoId),
},
title: `${entry.repoId.owner}/${entry.repoId.name} • Timeline`,
navTitle: null,
})),
{
path: "/discord-invite/",
contents: {
@ -99,6 +108,10 @@ function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
"FileUploader",
() => require("../util/FileUploaderInspectionTest").default
),
inspectionTestFor(
"TimelineCredView",
() => require("../explorer/TimelineCredViewInspectionTest").default
),
];
}
exports.makeRouteData = makeRouteData;

224
yarn.lock
View File

@ -735,6 +735,13 @@
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/runtime@^7.1.2":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.4.5":
version "7.5.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.1.tgz#51b56e216e87103ab3f7d6040b464c538e242888"
@ -1614,6 +1621,11 @@ bail@^1.0.0:
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.4.tgz#7181b66d508aa3055d3f6c13f0a0c720641dde9b"
integrity sha512-S8vuDB4w6YpRhICUDET3guPlQpaJl7od94tpZ0Fvnyp+MKW/HyDTcRDck+29C9g+d/qQHnddRH3+94kZdrW0Ww==
balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -2065,6 +2077,11 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
@ -2307,6 +2324,11 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.5.1:
version "2.6.9"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2470,12 +2492,89 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
d3-array@^2.2.0:
d3-array@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
"d3-array@^1.2.0 || 2", d3-array@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.2.0.tgz#a9e966b8f8d78f0888d98db1fb840fc8da8ac5c7"
integrity sha512-eE0QmSh6xToqM3sxHiJYg/QFdNn52ZEgmFE8A8abU8GsHvsIOolqH8B70/8+VGAKm5MlwaExhqR3DLIjOJMLPA==
d3-time@^1.0.11:
d3-collection@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
d3-color@1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f"
integrity sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==
d3-format@1, d3-format@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562"
integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==
d3-interpolate@1, d3-interpolate@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68"
integrity sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==
dependencies:
d3-color "1"
d3-path@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8"
integrity sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA==
d3-scale-chromatic@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0"
integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==
dependencies:
d3-color "1"
d3-interpolate "1"
d3-scale@^2.1.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
dependencies:
d3-array "^1.2.0"
d3-collection "1"
d3-format "1"
d3-interpolate "1"
d3-time "1"
d3-time-format "2"
d3-scale@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.0.0.tgz#ddede1278ac3ea2bf3666de6ca625e20bed9b6c9"
integrity sha512-ktic5HBFlAZj2CN8CCl/p/JyY8bMQluN7+fA6ICE6yyoMOnSQAZ1Bb8/5LcNpNKMBMJge+5Vv4pWJhARYlQYFw==
dependencies:
d3-array "^1.2.0 || 2"
d3-format "1"
d3-interpolate "1"
d3-time "1"
d3-time-format "2"
d3-shape@^1.2.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.5.tgz#e81aea5940f59f0a79cfccac012232a8987c6033"
integrity sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==
dependencies:
d3-path "1"
d3-time-format@2, d3-time-format@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b"
integrity sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==
dependencies:
d3-time "1"
d3-time@1, d3-time@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce"
integrity sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==
@ -2532,6 +2631,11 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decimal.js-light@^2.4.1:
version "2.5.0"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348"
integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -2726,6 +2830,13 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-serializer@0, dom-serializer@~0.1.0, dom-serializer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
@ -5250,6 +5361,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.defaults@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@ -5320,7 +5436,17 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.3.0:
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
"lodash@>=3.5 <5", lodash@^4.15.0, lodash@^4.17.4, lodash@^4.3.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.4:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@ -5396,6 +5522,11 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
math-expression-evaluator@^1.2.14:
version "1.2.17"
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw=
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -6518,7 +6649,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.6, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.6, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -6756,6 +6887,11 @@ react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^4.0.8:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-4.1.0.tgz#7fdf840ecbabc803f28156f7411c726b58f25a73"
@ -6769,6 +6905,16 @@ react-markdown@^4.0.8:
unist-util-visit "^1.3.0"
xtend "^4.0.1"
react-resize-detector@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==
dependencies:
lodash.debounce "^4.0.8"
lodash.throttle "^4.1.1"
prop-types "^15.6.0"
resize-observer-polyfill "^1.5.0"
react-router@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.1.tgz#b9a3279962bdfbe684c8bd0482b81ef288f0f244"
@ -6782,6 +6928,16 @@ react-router@3.2.1:
prop-types "^15.5.6"
warning "^3.0.0"
react-smooth@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.2.tgz#f7a2d932ece8db898646078c3c97f3e9533e0486"
integrity sha512-pIGzL1g9VGAsRsdZQokIK0vrCkcdKtnOnS1gyB2rrowdLy69lNSWoIjCTWAfgbiYvria8tm5hEZqj+jwXMkV4A==
dependencies:
lodash "~4.17.4"
prop-types "^15.6.0"
raf "^3.4.0"
react-transition-group "^2.5.0"
react-test-renderer@^16.0.0-0:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
@ -6792,6 +6948,16 @@ react-test-renderer@^16.0.0-0:
react-is "^16.8.6"
scheduler "^0.13.6"
react-transition-group@^2.5.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react@^16.4.1:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
@ -6874,6 +7040,30 @@ realpath-native@^1.1.0:
dependencies:
util.promisify "^1.0.0"
recharts-scale@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.2.tgz#b66315d985cd9b80d5f7d977a5aab9a305abc354"
integrity sha512-p/cKt7j17D1CImLgX2f5+6IXLbRHGUQkogIp06VUoci/XkhOQiGSzUrsD1uRmiI7jha4u8XNFOjkHkzzBPivMg==
dependencies:
decimal.js-light "^2.4.1"
recharts@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.6.2.tgz#4ced884f04b680e8dac5d3e109f99b0e7cfb9b0f"
integrity sha512-NqVN8Hq5wrrBthTxQB+iCnZjup1dc+AYRIB6Q9ck9UjdSJTt4PbLepGpudQEYJEN5iIpP/I2vThC4uiTJa7xUQ==
dependencies:
classnames "^2.2.5"
core-js "^2.5.1"
d3-interpolate "^1.3.0"
d3-scale "^2.1.0"
d3-shape "^1.2.0"
lodash "^4.17.5"
prop-types "^15.6.0"
react-resize-detector "^2.3.0"
react-smooth "^1.0.0"
recharts-scale "^0.4.2"
reduce-css-calc "^1.3.0"
recursive-readdir@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
@ -6881,6 +7071,22 @@ recursive-readdir@2.2.1:
dependencies:
minimatch "3.0.3"
reduce-css-calc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=
dependencies:
balanced-match "^0.4.2"
math-expression-evaluator "^1.2.14"
reduce-function-call "^1.0.1"
reduce-function-call@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99"
integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=
dependencies:
balanced-match "^0.4.2"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
@ -6973,6 +7179,11 @@ remark-parse@^5.0.0:
vfile-location "^2.0.0"
xtend "^4.0.1"
remove-markdown@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98"
integrity sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -7060,6 +7271,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"