diff --git a/changelog/12437.txt b/changelog/12437.txt new file mode 100644 index 000000000..d329e1b6b --- /dev/null +++ b/changelog/12437.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: creates bar chart component for displaying client count data by namespace +``` \ No newline at end of file diff --git a/ui/app/styles/components/bar-chart.scss b/ui/app/styles/components/bar-chart.scss new file mode 100644 index 000000000..1cc9bc126 --- /dev/null +++ b/ui/app/styles/components/bar-chart.scss @@ -0,0 +1,68 @@ +.bar-chart-wrapper { + border: $light-border; + border-radius: $radius-large; + padding: $spacing-l $spacing-l $spacing-s $spacing-l; + height: 100%; + width: 100%; + + > div.is-border { + border: 0.3px solid $ui-gray-200; + width: 94%; + margin-left: 3%; + margin-bottom: $spacing-xxs; + } +} + +.chart-header { + margin-left: $spacing-l; + display: grid; + grid-template-columns: 3fr 1fr; + + .header-left { + .chart-title { + font-size: $size-5; + font-weight: $font-weight-bold; + line-height: normal; + } + + .chart-description { + font-size: $size-8; + font-weight: $font-weight-normal; + color: $ui-gray-700; + margin-bottom: $spacing-xs; + } + } + + .header-right { + text-align: center; + + > button { + font-size: $size-8; + + &:hover { + text-decoration: underline; + } + } + } +} + +.bar-chart-container { + padding: $spacing-m $spacing-l $spacing-m $spacing-l; +} + +.bar-chart { + .tick > text { + font-weight: $font-weight-semibold; + font-size: $size-8; + } +} + +.legend-container { + height: $spacing-l; + margin-top: $spacing-xs; +} + +.legend { + width: 100%; + height: 100%; +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index cc4400750..06b5f9a15 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -45,6 +45,7 @@ @import './components/auth-buttons'; @import './components/auth-form'; @import './components/b64-toggle'; +@import './components/bar-chart'; @import './components/box-label'; @import './components/box-radio'; @import './components/codemirror'; diff --git a/ui/lib/core/addon/components/bar-chart.js b/ui/lib/core/addon/components/bar-chart.js new file mode 100644 index 000000000..3e58d3e26 --- /dev/null +++ b/ui/lib/core/addon/components/bar-chart.js @@ -0,0 +1,327 @@ +/** + * @module BarChart + * BarChart components are used to display data in the form of a stacked bar chart, with accompanying legend and tooltip. Anything passed into the block will display in the top right of the chart header. + * + * @example + * ```js + * + * /> + * + * + * mapLegendSample = [{ + * key: "api_key_for_label", + * label: "Label Displayed on Legend" + * }] + * ``` + * + * @param {string} title - title of the chart + * @param {array} mapLegend - array of objects with key names 'key' and 'label' for the map legend + * @param {object} dataset - dataset for the chart + * @param {string} [description] - description of the chart + * @param {string} [labelKey=label] - labelKey is the key name in the dataset passed in that corresponds to the value labeling the y-axis + * @param {function} [onClick] - takes function from parent and passes it to click event on data bars + * + */ + +import Component from '@glimmer/component'; +import layout from '../templates/components/bar-chart'; +import { setComponentTemplate } from '@ember/component'; +import { assert } from '@ember/debug'; +import { action } from '@ember/object'; +import { guidFor } from '@ember/object/internals'; +import { scaleLinear, scaleBand } from 'd3-scale'; +import { axisLeft } from 'd3-axis'; +import { max } from 'd3-array'; +import { stack } from 'd3-shape'; +// eslint-disable-next-line no-unused-vars +import { select, event, selectAll } from 'd3-selection'; +// eslint-disable-next-line no-unused-vars +import { transition } from 'd3-transition'; + +// SIZING CONSTANTS +const CHART_MARGIN = { top: 10, left: 137 }; // makes space for y-axis legend +const CHAR_LIMIT = 18; // character count limit for y-axis labels to trigger truncating +const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick + +// COLOR THEME: +const BAR_COLOR_DEFAULT = ['#BFD4FF', '#8AB1FF']; +const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1']; +const BACKGROUND_BAR_COLOR = '#EBEEF2'; +const TOOLTIP_BACKGROUND = '#525761'; + +class BarChartComponent extends Component { + get labelKey() { + return this.args.labelKey || 'label'; + } + + get mapLegend() { + assert( + 'map legend is required, must be an array of objects with key names of "key" and "label"', + this.hasLegend() + ); + return this.args.mapLegend; + } + + get dataset() { + return this.args.dataset || null; + } + + hasLegend() { + if (!this.args.mapLegend || !Array.isArray(this.args.mapLegend)) { + return false; + } else { + let legendKeys = this.args.mapLegend.map(obj => Object.keys(obj)); + return legendKeys.map(array => array.includes('key', 'label')).every(element => element === true); + } + } + + @action + renderBarChart(element) { + let elementId = guidFor(element); + let totalCount = this.dataset.reduce((prevValue, currValue) => prevValue + currValue.total, 0); + let handleClick = this.args.onClick; + let labelKey = this.labelKey; + let dataset = this.dataset; + let stackFunction = stack().keys(this.mapLegend.map(l => l.key)); + // creates an array of data for each map legend key + // each array contains coordinates for each data bar + let stackedData = stackFunction(dataset); + + // creates and appends tooltip + let container = select('.bar-chart-container'); + container + .append('div') + .attr('class', 'chart-tooltip') + .attr('style', 'position: fixed; opacity: 0;') + .style('color', 'white') + .style('background', `${TOOLTIP_BACKGROUND}`) + .style('font-size', '.929rem') + .style('padding', '10px') + .style('border-radius', '4px'); + + let xScale = scaleLinear() + .domain([0, max(dataset.map(d => d.total))]) + .range([0, 75]); // 25% reserved for margins + + let yScale = scaleBand() + .domain(dataset.map(d => d[labelKey])) + .range([0, dataset.length * LINE_HEIGHT]) + .paddingInner(0.765); // percent of the total width to reserve for padding between bars + + let chartSvg = select(element); + chartSvg.attr('viewBox', `0 0 710 ${(dataset.length + 1) * LINE_HEIGHT}`); + chartSvg.attr('id', elementId); + + // creates group for each array of stackedData + let groups = chartSvg + .selectAll('g') + .data(stackedData) + .enter() + .append('g') + // shifts chart to accommodate y-axis legend + .attr('transform', `translate(${CHART_MARGIN.left}, ${CHART_MARGIN.top})`) + .style('fill', (d, i) => BAR_COLOR_DEFAULT[i]); + + let yAxis = axisLeft(yScale); + yAxis(groups.append('g')); + + let truncate = selection => + selection.text(string => + string.length < CHAR_LIMIT ? string : string.slice(0, CHAR_LIMIT - 3) + '...' + ); + + chartSvg.selectAll('.tick text').call(truncate); + + let rects = groups + .selectAll('rect') + .data(d => d) + .enter() + .append('rect') + .attr('class', 'data-bar') + .style('cursor', 'pointer') + .attr('width', chartData => `${xScale(chartData[1] - chartData[0] - 5)}%`) + .attr('height', yScale.bandwidth()) + .attr('x', chartData => `${xScale(chartData[0])}%`) + .attr('y', ({ data }) => yScale(data[labelKey])) + .attr('rx', 3) + .attr('ry', 3); + + let actionBars = chartSvg + .selectAll('.action-bar') + .data(dataset) + .enter() + .append('rect') + .style('cursor', 'pointer') + .attr('class', 'action-bar') + .attr('width', '100%') + .attr('height', `${LINE_HEIGHT}px`) + .attr('x', '0') + .attr('y', chartData => yScale(chartData[labelKey])) + .style('fill', `${BACKGROUND_BAR_COLOR}`) + .style('opacity', '0') + .style('mix-blend-mode', 'multiply'); + + let yLegendBars = chartSvg + .selectAll('.label-bar') + .data(dataset) + .enter() + .append('rect') + .style('cursor', 'pointer') + .attr('class', 'label-action-bar') + .attr('width', CHART_MARGIN.left) + .attr('height', `${LINE_HEIGHT}px`) + .attr('x', '0') + .attr('y', chartData => yScale(chartData[labelKey])) + .style('opacity', '0') + .style('mix-blend-mode', 'multiply'); + + let dataBars = chartSvg.selectAll('rect.data-bar'); + let actionBarSelection = chartSvg.selectAll('rect.action-bar'); + let compareAttributes = (elementA, elementB, attr) => + select(elementA).attr(`${attr}`) === elementB.getAttribute(`${attr}`); + + // handles click and mouseover/out/move event for data bars + actionBars + .on('click', function(chartData) { + if (handleClick) { + handleClick(chartData); + } + }) + .on('mouseover', function() { + select(this).style('opacity', 1); + dataBars + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`); + select('.chart-tooltip') + .transition() + .duration(200) + .style('opacity', 1); + }) + .on('mouseout', function() { + select(this).style('opacity', 0); + select('.chart-tooltip').style('opacity', 0); + dataBars + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`); + }) + .on('mousemove', function(chartData) { + select('.chart-tooltip') + .style('opacity', 1) + .style('max-width', '200px') + .style('left', `${event.pageX - 90}px`) + .style('top', `${event.pageY - 90}px`) + .text( + `${Math.round((chartData.total * 100) / totalCount)}% of total client counts: + ${chartData.distinct_entities} unique entities, ${chartData.non_entity_tokens} active tokens. + ` + ); + }); + + // handles mouseover/out/move event for y-axis legend + yLegendBars + .on('click', function(chartData) { + if (handleClick) { + handleClick(chartData); + } + }) + .on('mouseover', function(chartData) { + dataBars + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('fill', (b, i) => `${BAR_COLOR_HOVER[i]}`); + actionBarSelection + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('opacity', '1'); + if (chartData.label.length >= CHAR_LIMIT) { + select('.chart-tooltip') + .transition() + .duration(200) + .style('opacity', 1); + } + }) + .on('mouseout', function() { + select('.chart-tooltip').style('opacity', 0); + dataBars + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('fill', (b, i) => `${BAR_COLOR_DEFAULT[i]}`); + actionBarSelection + .filter(function() { + return compareAttributes(this, event.target, 'y'); + }) + .style('opacity', '0'); + }) + .on('mousemove', function(chartData) { + if (chartData.label.length >= CHAR_LIMIT) { + select('.chart-tooltip') + .style('left', `${event.pageX - 100}px`) + .style('top', `${event.pageY - 50}px`) + .text(`${chartData.label}`) + .style('max-width', 'fit-content'); + } else { + select('.chart-tooltip').style('opacity', 0); + } + }); + + // TODO: these render twice, need to only render and append once per line + // creates total count text and coordinates to display to the right of data bars + let totalCountData = []; + rects.each(function(d) { + let textDatum = { + total: d.data.total, + x: parseFloat(select(this).attr('width')) + parseFloat(select(this).attr('x')), + y: parseFloat(select(this).attr('y')) + parseFloat(select(this).attr('height')), + }; + totalCountData.push(textDatum); + }); + + groups + .selectAll('text') + .data(totalCountData) + .enter() + .append('text') + .text(d => d.total) + .attr('fill', '#000') + .attr('class', 'total-value') + .style('font-size', '.8rem') + .attr('text-anchor', 'start') + .attr('y', d => `${d.y}`) + .attr('x', d => `${d.x + 1}%`); + + // removes axes lines + groups.selectAll('.domain, .tick line').remove(); + + // TODO: make more flexible, make legend a div instead of svg? + // each map key symbol & label takes up 20% of legend SVG width + let startingXCoordinate = 100 - this.mapLegend.length * 20; // subtract from 100% to find starting x-coordinate + let legendSvg = select('.legend'); + this.mapLegend.map((legend, i) => { + let xCoordinate = startingXCoordinate + i * 20; + legendSvg + .append('circle') + .attr('cx', `${xCoordinate}%`) + .attr('cy', '50%') + .attr('r', 6) + .style('fill', `${BAR_COLOR_DEFAULT[i]}`); + legendSvg + .append('text') + .attr('x', `${xCoordinate + 2}%`) + .attr('y', '50%') + .text(`${legend.label}`) + .style('font-size', '.8rem') + .attr('alignment-baseline', 'middle'); + }); + } +} + +export default setComponentTemplate(layout, BarChartComponent); diff --git a/ui/lib/core/addon/templates/components/bar-chart.hbs b/ui/lib/core/addon/templates/components/bar-chart.hbs new file mode 100644 index 000000000..0955be26e --- /dev/null +++ b/ui/lib/core/addon/templates/components/bar-chart.hbs @@ -0,0 +1,28 @@ +
+
+
+

{{@title}}

+ {{#if @description}} +

{{@description}}

+ {{/if}} +
+
+ {{#if this.dataset}} + {{yield}} + {{/if}} +
+
+ {{#unless this.dataset}} +
+ + {{else}} +
+
+ +
+
+
+ +
+ {{/unless}} +
diff --git a/ui/lib/core/app/components/bar-chart.js b/ui/lib/core/app/components/bar-chart.js new file mode 100644 index 000000000..085613131 --- /dev/null +++ b/ui/lib/core/app/components/bar-chart.js @@ -0,0 +1 @@ +export { default } from 'core/components/bar-chart'; diff --git a/ui/lib/core/stories/bar-chart.md b/ui/lib/core/stories/bar-chart.md new file mode 100644 index 000000000..1653f3c5a --- /dev/null +++ b/ui/lib/core/stories/bar-chart.md @@ -0,0 +1,37 @@ + + +## BarChart +BarChart components are used to display data in the form of a stacked bar chart, with accompanying legend and tooltip. Anything passed into the block will display in the top right of the chart header. + +**Params** + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| title | string | | title of the chart | +| mapLegend | array | | array of objects with key names 'key' and 'label' for the map legend | +| dataset | object | | dataset for the chart | +| [description] | string | | description of the chart | +| [labelKey] | string | "label" | labelKey is the key name in the dataset passed in that corresponds to the value labeling the y-axis | +| [onClick] | function | | takes function from parent and passes it to click event on data bars | + +**Example** + +```js + + /> + + + mapLegendSample = [{ + key: "api_key_for_label", + label: "Label Displayed on Legend" + }] +``` + +**See** + +- [Uses of BarChart](https://github.com/hashicorp/vault/search?l=Handlebars&q=BarChart+OR+bar-chart) +- [BarChart Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/bar-chart.js) + +--- diff --git a/ui/lib/core/stories/bar-chart.stories.js b/ui/lib/core/stories/bar-chart.stories.js new file mode 100644 index 000000000..382fbe4d3 --- /dev/null +++ b/ui/lib/core/stories/bar-chart.stories.js @@ -0,0 +1,99 @@ +import hbs from 'htmlbars-inline-precompile'; +import { storiesOf } from '@storybook/ember'; +import { object, text, withKnobs } from '@storybook/addon-knobs'; +import notes from './bar-chart.md'; + +const dataset = [ + { + namespace_id: 'root', + namespace_path: 'root', + counts: { + distinct_entities: 268, + non_entity_tokens: 985, + clients: 1253, + }, + }, + { + namespace_id: 'O0i4m', + namespace_path: 'top-namespace', + counts: { + distinct_entities: 648, + non_entity_tokens: 220, + clients: 868, + }, + }, + { + namespace_id: '1oihz', + namespace_path: 'anotherNamespace', + counts: { + distinct_entities: 547, + non_entity_tokens: 337, + clients: 884, + }, + }, + { + namespace_id: '1oihz', + namespace_path: 'someOtherNamespaceawgagawegawgawgawgaweg', + counts: { + distinct_entities: 807, + non_entity_tokens: 234, + clients: 1041, + }, + }, +]; + +const flattenData = () => { + return dataset.map(d => { + return { + label: d['namespace_path'], + non_entity_tokens: d['counts']['non_entity_tokens'], + distinct_entities: d['counts']['distinct_entities'], + total: d['counts']['clients'], + }; + }); +}; + +storiesOf('BarChart', module) + .addParameters({ options: { showPanel: true } }) + .addDecorator(withKnobs()) + .add( + `BarChart`, + () => ({ + template: hbs` +
Bar Chart
+ +

dataset is passed to a function in the parent to format it appropriately for the chart. Any data passed should be flattened (not nested).

+

The legend typically displays within the bar chart border, below the second grey divider. There is also a tooltip that pops up when hovering over the data bars and overflowing labels. Gotta love storybook :)

+
+ + + +
+
Legend:
+ + + Active direct tokens + + Unique Entities +
+ `, + context: { + title: text('title', 'Top Namespaces'), + description: text( + 'description', + 'Each namespaces client count includes clients in child namespaces.' + ), + dataset: object('dataset', flattenData()), + }, + }), + { notes } + ); diff --git a/ui/package.json b/ui/package.json index 05225e58a..ab578dd7e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -97,6 +97,7 @@ "ember-concurrency-test-waiter": "^0.3.2", "ember-copy": "^1.0.0", "ember-cp-validations": "^4.0.0-beta.12", + "ember-d3": "^0.5.1", "ember-data": "~3.22.0", "ember-data-model-fragments": "5.0.0-beta.0", "ember-engines": "^0.8.3", diff --git a/ui/tests/integration/components/bar-chart-test.js b/ui/tests/integration/components/bar-chart-test.js new file mode 100644 index 000000000..dda99be3b --- /dev/null +++ b/ui/tests/integration/components/bar-chart-test.js @@ -0,0 +1,81 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | bar-chart', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + let dataset = [ + { + namespace_id: 'root', + namespace_path: 'root', + counts: { + distinct_entities: 268, + non_entity_tokens: 985, + clients: 1253, + }, + }, + { + namespace_id: 'O0i4m', + namespace_path: 'top-namespace', + counts: { + distinct_entities: 648, + non_entity_tokens: 220, + clients: 868, + }, + }, + { + namespace_id: '1oihz', + namespace_path: 'anotherNamespace', + counts: { + distinct_entities: 547, + non_entity_tokens: 337, + clients: 884, + }, + }, + { + namespace_id: '1oihz', + namespace_path: 'someOtherNamespaceawgagawegawgawgawgaweg', + counts: { + distinct_entities: 807, + non_entity_tokens: 234, + clients: 1041, + }, + }, + ]; + + let flattenData = () => { + return dataset.map(d => { + return { + label: d['namespace_path'], + non_entity_tokens: d['counts']['non_entity_tokens'], + distinct_entities: d['counts']['distinct_entities'], + total: d['counts']['clients'], + }; + }); + }; + + this.set('title', 'Top Namespaces'); + this.set('description', 'Each namespaces client count includes clients in child namespaces.'); + this.set('dataset', flattenData()); + + await render(hbs` + + + + `); + + assert.dom('.bar-chart-wrapper').exists('it renders'); + }); +}); diff --git a/ui/yarn.lock b/ui/yarn.lock index bcb121b06..8c5a50242 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -6288,6 +6288,11 @@ command-line-usage@^4.1.0: table-layout "^0.4.2" typical "^2.6.1" +commander@2, commander@^2.15.1, commander@^2.20.0, commander@^2.6.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -6300,11 +6305,6 @@ commander@7.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== -commander@^2.15.1, commander@^2.20.0, commander@^2.6.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -6853,16 +6853,35 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -d3-array@^1.2.0: +d3-array@1, d3-array@^1.1.1, 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-axis@^1.0.8: +d3-axis@1, d3-axis@^1.0.8: version "1.0.12" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== +d3-brush@1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b" + integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + d3-collection@1, d3-collection@^1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" @@ -6873,21 +6892,74 @@ d3-color@1: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + d3-dispatch@1: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== +d3-drag@1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" + integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" + integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + d3-ease@1, d3-ease@^1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== +d3-fetch@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7" + integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + d3-format@1: version "1.4.5" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== +d3-geo@1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f" + integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg== + dependencies: + d3-array "1" + +d3-hierarchy@1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" + integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== + d3-interpolate@1: version "1.4.0" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" @@ -6895,6 +6967,46 @@ d3-interpolate@1: dependencies: d3-color "1" +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-polygon@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + +d3-quadtree@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135" + integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA== + +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + +d3-scale-chromatic@1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" + integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + 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@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" @@ -6908,11 +7020,26 @@ d3-scale@^1.0.7: d3-time "1" d3-time-format "2" -d3-selection@^1.1.0, d3-selection@^1.3.0: +d3-selection-multi@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58" + integrity sha1-zWwlQT0EosuXRw54byzYd/PjT1g= + dependencies: + d3-selection "1" + d3-transition "1" + +d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.3.0: version "1.4.2" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== +d3-shape@1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + d3-time-format@2, d3-time-format@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" @@ -6938,7 +7065,7 @@ d3-tip@^0.9.1: d3-collection "^1.0.4" d3-selection "^1.3.0" -d3-transition@^1.2.0: +d3-transition@1, d3-transition@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== @@ -6950,6 +7077,59 @@ d3-transition@^1.2.0: d3-selection "^1.1.0" d3-timer "1" +d3-voronoi@1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + +d3-zoom@1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" + integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^5.0.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" + integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dag-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68" @@ -8252,6 +8432,17 @@ ember-cp-validations@^4.0.0-beta.12: ember-require-module "^0.3.0" ember-validators "^3.0.1" +ember-d3@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/ember-d3/-/ember-d3-0.5.1.tgz#b23ce145863f082b5e73d25d9a43a0f1d9e9f412" + integrity sha512-NyjTUuIOxGxZdyrxLasNwwjqyFgay1pVHGRAWFj7mriwTI44muKsM9ZMl6YeepqixceuFig2fDxHmLLrkQV+QQ== + dependencies: + broccoli-funnel "^2.0.0" + broccoli-merge-trees "^3.0.0" + d3 "^5.0.0" + d3-selection-multi "^1.0.1" + ember-cli-babel "^7.1.2" + ember-data-model-fragments@5.0.0-beta.0: version "5.0.0-beta.0" resolved "https://registry.yarnpkg.com/ember-data-model-fragments/-/ember-data-model-fragments-5.0.0-beta.0.tgz#da90799970317ca852f96b2ea1548ca70094a5bb" @@ -11075,7 +11266,7 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -15938,6 +16129,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= + rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"