From 5dc7f440cef29fbed5dda51a8aeceaff417c7b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Fri, 31 May 2019 20:17:27 +0300 Subject: [PATCH] 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). --- package.json | 6 + src/explorer/TimelineApp.js | 104 ++++++++ src/explorer/TimelineCredChart.js | 126 ++++++++++ src/explorer/TimelineCredView.js | 78 ++++++ .../TimelineCredViewInspectionTest.js | 73 ++++++ src/explorer/TimelineExplorer.js | 160 +++++++++++++ src/homepage/TimelinePage.js | 17 ++ src/homepage/homepageTimeline.js | 22 ++ src/homepage/routeData.js | 13 + yarn.lock | 224 +++++++++++++++++- 10 files changed, 819 insertions(+), 4 deletions(-) create mode 100644 src/explorer/TimelineApp.js create mode 100644 src/explorer/TimelineCredChart.js create mode 100644 src/explorer/TimelineCredView.js create mode 100644 src/explorer/TimelineCredViewInspectionTest.js create mode 100644 src/explorer/TimelineExplorer.js create mode 100644 src/homepage/TimelinePage.js create mode 100644 src/homepage/homepageTimeline.js diff --git a/package.json b/package.json index fa54f2c..96e8e4a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/explorer/TimelineApp.js b/src/explorer/TimelineApp.js new file mode 100644 index 0000000..0464743 --- /dev/null +++ b/src/explorer/TimelineApp.js @@ -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; + +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 { + 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 ( +
+

Loading...

+
+ ); + } + case "ERROR": { + const {error} = loadResult; + return ( +
+

Load Error:

+

+ {error.status}:{error.statusText} +

+
+ ); + } + case "SUCCESS": { + const {timelineCred} = loadResult; + return ( + + ); + } + default: + throw new Error(`Unexpected load state: ${(loadResult.type: empty)}`); + } + } +} + +export async function defaultLoader( + assets: Assets, + repoId: RepoId +): Promise { + async function fetchCred(): Promise { + 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}; + } +} diff --git a/src/explorer/TimelineCredChart.js b/src/explorer/TimelineCredChart.js new file mode 100644 index 0000000..544fba9 --- /dev/null +++ b/src/explorer/TimelineCredChart.js @@ -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, +}; + +type LineChartDatum = { + interval: Interval, + // May be null if the node score was filtered out (effectively bc. + // that node did not exist yet) + score: Map, +}; + +// 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 { + 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 ( + 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 ( + + + x.interval.startTimeMs} + type="number" + domain={timeDomain} + tickFormatter={multiFormat} + ticks={ticks} + /> + + {Lines} + -x.value} + labelFormatter={(v) => { + return `Week of ${timeFormat("%B %e, %Y")(v)}`; + }} + /> + + + ); + } +} diff --git a/src/explorer/TimelineCredView.js b/src/explorer/TimelineCredView.js new file mode 100644 index 0000000..08b7638 --- /dev/null +++ b/src/explorer/TimelineCredView.js @@ -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 { + 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 ( +
+ + + + + + + + + + + {tableNodes.map(({node, total}) => { + return ( + + + + + + ); + })} + +
ContributorCred% Total
+ + {format(".1d")(total)} + {format(".1%")(total / totalScore)} +
+
+ ); + } +} diff --git a/src/explorer/TimelineCredViewInspectionTest.js b/src/explorer/TimelineCredViewInspectionTest.js new file mode 100644 index 0000000..00a1fcf --- /dev/null +++ b/src/explorer/TimelineCredViewInspectionTest.js @@ -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 ( + + ); + } +} diff --git a/src/explorer/TimelineExplorer.js b/src/explorer/TimelineExplorer.js new file mode 100644 index 0000000..3faaea7 --- /dev/null +++ b/src/explorer/TimelineExplorer.js @@ -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, +}; + +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 { + 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 = ( + { + this.setState({weights}); + }} + /> + ); + const weightConfig = ( + { + 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 = ( + + ); + const {owner, name} = this.props.repoId; + return ( +
+
+ + cred for {owner}/{name} + (legacy) + + + + {analyzeButton} +
+ {showWeightConfig && ( +
+ Upload/Download weights: + {weightFileManager} + {weightConfig} +
+ )} +
+ ); + } + + render() { + const timelineCredView = ( + + ); + return ( +
+ {this.renderConfigurationRow()} + {timelineCredView} +
+ ); + } +} diff --git a/src/homepage/TimelinePage.js b/src/homepage/TimelinePage.js new file mode 100644 index 0000000..2bc4933 --- /dev/null +++ b/src/homepage/TimelinePage.js @@ -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 ; + } + }; +} diff --git a/src/homepage/homepageTimeline.js b/src/homepage/homepageTimeline.js new file mode 100644 index 0000000..09d0081 --- /dev/null +++ b/src/homepage/homepageTimeline.js @@ -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 ( + + ); + } +} diff --git a/src/homepage/routeData.js b/src/homepage/routeData.js index 867ccd2..a905bd5 100644 --- a/src/homepage/routeData.js +++ b/src/homepage/routeData.js @@ -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; diff --git a/yarn.lock b/yarn.lock index 6bae52d..f69941e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"