Browse Source

Merge branch 'new-core-models' into 'main'

update core to 0.6.x

See merge request chartwerk/scatter-pod!5
merge-requests/6/merge
rozetko 2 years ago
parent
commit
a89e2cf8d6
  1. 10
      examples/demo.html
  2. 3
      package.json
  3. 150
      src/index.ts
  4. 13
      src/models/delaunay.ts
  5. 23
      src/models/scatter_series.ts
  6. 6
      src/types.ts
  7. 6
      yarn.lock

10
examples/demo.html

@ -45,11 +45,13 @@
axis: { axis: {
x: { x: {
format: 'numeric', format: 'numeric',
range: [-100, 300] range: [-100, 300],
label: 'x'
}, },
y: { y: {
invert: true, invert: true,
range: [-100, 250] range: [-100, 250],
label: 'y'
}, },
y1: { y1: {
isActive: true, isActive: true,
@ -70,10 +72,6 @@
orientation: 'both', orientation: 'both',
color: 'gray' color: 'gray'
}, },
labelFormat: {
yAxis: 'y',
xAxis: 'x'
},
eventsCallbacks: { eventsCallbacks: {
zoomOut: () => { pod.render() } zoomOut: () => { pod.render() }
}, },

3
package.json

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"build": "webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js", "build": "webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js", "dev": "webpack --watch --config build/webpack.dev.conf.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"update-core": "yarn up @chartwerk/core && yarn up @chartwerk/core@latest"
}, },
"repository": {}, "repository": {},
"author": "CorpGlory", "author": "CorpGlory",

150
src/index.ts

@ -1,17 +1,14 @@
import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, yAxisOrientation, CrosshairOrientation } from '@chartwerk/core'; import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, yAxisOrientation, CrosshairOrientation } from '@chartwerk/core';
import { ScatterData, ScatterOptions, PointType, LineType } from './types'; import { ScatterData, ScatterOptions, PointType, LineType } from './types';
import { DelaunayDiagram } from './delaunay'; import { DelaunayDiagram } from './models/delaunay';
import { ScatterSeries } from './models/scatter_series';
import * as d3 from 'd3'; import * as d3 from 'd3';
import * as _ from 'lodash'; import * as _ from 'lodash';
// TODO: use pod state with defaults
const DEFAULT_POINT_SIZE = 4;
const POINT_HIGHLIGHT_DIAMETER = 4; const POINT_HIGHLIGHT_DIAMETER = 4;
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; const CROSSHAIR_BACKGROUND_OPACITY = 0.3;
const DEFAULT_POINT_TYPE = PointType.CIRCLE;
const DEFAULT_LINE_TYPE = LineType.NONE;
const DEFAULT_LINE_DASHED_AMOUNT = 4; const DEFAULT_LINE_DASHED_AMOUNT = 4;
export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> { export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> {
@ -20,16 +17,17 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) { constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) {
super(el, _series, _options); super(el, _series, _options);
this.series = new ScatterSeries(_series);
} }
renderMetrics(): void { renderMetrics(): void {
if(this.series.length === 0) { if(!this.series.isSeriesAvailable) {
this.renderNoDataPointsMessage(); this.renderNoDataPointsMessage();
return; return;
} }
this.updateCrosshair(); this.updateCrosshair();
this._delaunayDiagram = new DelaunayDiagram(this.series, this.xScale, this.getYScale.bind(this)); this._delaunayDiagram = new DelaunayDiagram(this.series.visibleSeries, this.state.xScale, this.getYScale.bind(this));
this.renderLines(); this.renderLines();
this.renderPoints(); this.renderPoints();
@ -47,21 +45,20 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
appendCrosshairPoints(): void { appendCrosshairPoints(): void {
this.series.forEach((serie: ScatterData, serieIdx: number) => { this.series.visibleSeries.forEach((serie: ScatterData) => {
this.appendCrosshairPoint(serieIdx); this.appendCrosshairPoint(serie.idx, serie.pointType, serie.pointSize);
}); });
} }
protected appendCrosshairPoint(serieIdx: number): void { protected appendCrosshairPoint(serieIdx: number, pointType: PointType, size: number): void {
// TODO: add Crosshair type options // TODO: add Crosshair type options
const pointType = this.series[serieIdx].pointType || DEFAULT_POINT_TYPE;
switch(pointType) { switch(pointType) {
case PointType.NONE: case PointType.NONE:
return; return;
case PointType.CIRCLE: case PointType.CIRCLE:
this.crosshair.append('circle') this.crosshair.append('circle')
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) .attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`)
.attr('r', this.getCrosshairCircleBackgroundSize(serieIdx)) .attr('r', this.getCrosshairCircleBackgroundSize(size, pointType))
.attr('clip-path', `url(#${this.rectClipId})`) .attr('clip-path', `url(#${this.rectClipId})`)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) .style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none') .style('pointer-events', 'none')
@ -70,8 +67,8 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
case PointType.RECTANGLE: case PointType.RECTANGLE:
this.crosshair.append('rect') this.crosshair.append('rect')
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) .attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`)
.attr('width', this.getCrosshairCircleBackgroundSize(serieIdx)) .attr('width', this.getCrosshairCircleBackgroundSize(size, pointType))
.attr('height', this.getCrosshairCircleBackgroundSize(serieIdx)) .attr('height', this.getCrosshairCircleBackgroundSize(size, pointType))
.attr('clip-path', `url(#${this.rectClipId})`) .attr('clip-path', `url(#${this.rectClipId})`)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) .style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none') .style('pointer-events', 'none')
@ -83,35 +80,31 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
protected renderLines(): void { protected renderLines(): void {
this.series.forEach((serie, serieIdx) => { this.series.visibleSeries.forEach(serie => {
if(serie.visible === false) { this.renderLine(serie);
return;
}
const lineType = serie.lineType || DEFAULT_LINE_TYPE;
this.renderLine(serie.datapoints, lineType, this.getSerieColor(serieIdx), serie.yOrientation);
}); });
} }
renderLine(datapoints: number[][], lineType: LineType, color: string, orientation: yAxisOrientation): void { renderLine(serie: ScatterData): void {
if(lineType === LineType.NONE) { if(serie.lineType === LineType.NONE) {
return; return;
} }
let strokeDasharray; let strokeDasharray;
// TODO: move to option // TODO: move to option
if(lineType === LineType.DASHED) { if(serie.lineType === LineType.DASHED) {
strokeDasharray = DEFAULT_LINE_DASHED_AMOUNT; strokeDasharray = DEFAULT_LINE_DASHED_AMOUNT;
} }
const lineGenerator = d3.line() const lineGenerator = d3.line()
.x((d: [number, number]) => this.xScale(d[0])) .x((d: [number, number]) => this.state.xScale(d[0]))
.y((d: [number, number]) => this.getYScale(orientation)(d[1])); .y((d: [number, number]) => this.getYScale(serie.yOrientation)(d[1]));
this.metricContainer this.metricContainer
.append('path') .append('path')
.datum(datapoints) .datum(serie.datapoints)
.attr('class', 'metric-path') .attr('class', 'metric-path')
.attr('fill', 'none') .attr('fill', 'none')
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('stroke', color) .attr('stroke', serie.color)
.attr('stroke-width', 1) .attr('stroke-width', 1)
.attr('stroke-opacity', 0.7) .attr('stroke-opacity', 0.7)
.attr('stroke-dasharray', strokeDasharray) .attr('stroke-dasharray', strokeDasharray)
@ -127,46 +120,47 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('circle') .append('circle')
.filter((d: number[]) => this.series[_.last(d)].pointType !== PointType.RECTANGLE) .filter((d: number[]) => this.getSerieByIdx(_.last(d))?.pointType !== PointType.RECTANGLE)
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) .attr('class', (d, i: number) => `metric-element metric-circle point-${i}`)
.attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) .attr('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.style('fill', (d: number[], i: number) => this.getSeriesColorFromDataRow(d, i)) .style('fill', (d: number[], i: number) => this.getSeriesColorFromDataRow(d, i))
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('cx', (d: any[]) => this.xScale(d[0])) .attr('cx', (d: any[]) => this.state.xScale(d[0]))
.attr('cy', (d: any[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[1])); .attr('cy', (d: any[]) => this.getYScale(this.getSerieByIdx(_.last(d))?.yOrientation)(d[1]));
this.metricContainer.selectAll(null) this.metricContainer.selectAll(null)
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('rect') .append('rect')
.filter((d: number[]) => this.series[_.last(d)].pointType === PointType.RECTANGLE) .filter((d: number[]) => this.getSerieByIdx(_.last(d))?.pointType === PointType.RECTANGLE)
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) .attr('class', (d, i: number) => `metric-element metric-circle point-${i}`)
.attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) .attr('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.style('fill', (d: number[]) => this.getSerieColor(_.last(d))) .style('fill', (d: number[]) => this.getSerieByIdx(_.last(d))?.color)
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('x', (d: number[]) => this.xScale(d[0]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2) .attr('x', (d: number[]) => this.state.xScale(d[0]) - this.getSerieByIdx(_.last(d))?.pointSize / 2)
.attr('y', (d: number[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[1]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2) .attr('y', (d: number[]) => this.getYScale(this.getSerieByIdx(_.last(d)).yOrientation)(d[1]) - this.getSerieByIdx(_.last(d))?.pointSize / 2)
.attr('width', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) .attr('width', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.attr('height', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE); .attr('height', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize);
}
getSerieByIdx(idx: number): ScatterData | undefined {
return _.find(this.series.visibleSeries, serie => serie.idx === idx);
} }
getSeriesColorFromDataRow(values: number[], rowIdx: number): string { getSeriesColorFromDataRow(values: number[], rowIdx: number): string {
const seriesIdx = _.last(values); const seriesIdx = _.last(values);
if(this.series[seriesIdx].colorFormatter) { const serie = this.getSerieByIdx(seriesIdx);
return this.series[seriesIdx].colorFormatter(values, rowIdx); if(serie?.colorFormatter) {
return serie.colorFormatter(values, rowIdx);
} }
return this.getSerieColor(seriesIdx); return serie.color;
} }
onPanningEnd(): void { onPanningEnd(): void {
this.isPanning = false; this.isPanning = false;
this.onMouseOut(); this.onMouseOut();
this._delaunayDiagram.setDelaunayDiagram(this.xScale, this.getYScale.bind(this)); this._delaunayDiagram.setDelaunayDiagram(this.state.xScale, this.getYScale.bind(this));
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.panningEnd !== undefined) { this.options.callbackPanningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]);
this.options.eventsCallbacks.panningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]);
} else {
console.log('on panning end, but there is no callback');
}
} }
unhighlight(): void { unhighlight(): void {
@ -182,40 +176,33 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
const serieIdx = _.last(datapoint); const serieIdx = _.last(datapoint);
const serieOrientation = this.series[serieIdx].yOrientation; const serie = this.getSerieByIdx(serieIdx);
const size = this.getCrosshairCircleBackgroundSize(serieIdx); const size = this.getCrosshairCircleBackgroundSize(serie.pointSize, serie.pointType);
this.crosshair.selectAll(`.crosshair-point-${serieIdx}`) this.crosshair.selectAll(`.crosshair-point-${serieIdx}`)
.attr('cx', this.xScale(datapoint[0])) .attr('cx', this.state.xScale(datapoint[0]))
.attr('cy', this.getYScale(serieOrientation)(datapoint[1])) .attr('cy', this.getYScale(serie.yOrientation)(datapoint[1]))
.attr('x', this.xScale(datapoint[0]) - size / 2) .attr('x', this.state.xScale(datapoint[0]) - size / 2)
.attr('y', this.getYScale(serieOrientation)(datapoint[1]) - size / 2) .attr('y', this.getYScale(serie.yOrientation)(datapoint[1]) - size / 2)
.attr('fill', this.getSeriesColorFromDataRow(datapoint, pointIdx)) .attr('fill', this.getSeriesColorFromDataRow(datapoint, pointIdx))
.style('display', null); .style('display', null);
} }
protected getCrosshairCircleBackgroundSize(serieIdx: number): number { protected getCrosshairCircleBackgroundSize(pointSize: number, pointType: PointType): number {
const seriePointSize = this.series[serieIdx].pointSize || DEFAULT_POINT_SIZE;
const pointType = this.series[serieIdx].pointType || DEFAULT_POINT_TYPE;
let highlightDiameter = POINT_HIGHLIGHT_DIAMETER; let highlightDiameter = POINT_HIGHLIGHT_DIAMETER;
if(pointType === PointType.RECTANGLE) { if(pointType === PointType.RECTANGLE) {
highlightDiameter = highlightDiameter * 2; highlightDiameter = highlightDiameter * 2;
} }
return seriePointSize + highlightDiameter; return pointSize + highlightDiameter;
} }
public renderSharedCrosshair(values: { x?: number, y?: number }): void { public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.onMouseOver(); // TODO: refactor to use it once this.onMouseOver(); // TODO: refactor to use it once
const eventX = this.xScale(values.x); const eventX = this.state.xScale(values.x);
const eventY = this.yScale(values.y); const eventY = this.state.yScale(values.y);
this.moveCrosshairLine(eventX, eventY); this.moveCrosshairLine(eventX, eventY);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.sharedCrosshairMove === undefined) { this.options.callbackSharedCrosshairMove({
console.log('Shared crosshair move, but there is no callback');
return;
}
this.options.eventsCallbacks.sharedCrosshairMove({
datapoints, datapoints,
eventX, eventY eventX, eventY
}); });
@ -247,7 +234,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
findAndHighlightDatapoints(eventX: number, eventY: number): { values: any[], pointIdx: number } | null { findAndHighlightDatapoints(eventX: number, eventY: number): { values: any[], pointIdx: number } | null {
if(this.series === undefined || this.series.length === 0) { if(!this.series.isSeriesAvailable) {
return null; return null;
} }
@ -265,14 +252,11 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number> { protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number> {
if(orientation === undefined || orientation === yAxisOrientation.BOTH) {
return this.yScale;
}
switch(orientation) { switch(orientation) {
case yAxisOrientation.LEFT: case yAxisOrientation.LEFT:
return this.yScale; return this.state.yScale;
case yAxisOrientation.RIGHT: case yAxisOrientation.RIGHT:
return this.y1Scale; return this.state.y1Scale;
default: default:
throw new Error(`Unknown type of y axis orientation: ${orientation}`) throw new Error(`Unknown type of y axis orientation: ${orientation}`)
} }
@ -287,8 +271,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
const eventX = mousePosition[0]; const eventX = mousePosition[0];
const eventY = mousePosition[1]; const eventY = mousePosition[1];
// TODO: seems isOutOfChart is deprecated (check clippath correctness) if(this.isPanning === true || this.isBrushing === true) {
if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) {
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
return; return;
} else { } else {
@ -299,16 +282,13 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
// TOOD: it should be two different methods // TOOD: it should be two different methods
const highlighted = this.findAndHighlightDatapoints(eventX, eventY); const highlighted = this.findAndHighlightDatapoints(eventX, eventY);
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) {
console.log('Mouse move, but there is no callback');
return;
}
// TODO: group fields // TODO: group fields
this.options.eventsCallbacks.mouseMove({ this.options.callbackMouseMove({
x: d3.event.clientX, x: d3.event.clientX,
y: d3.event.clientY, y: d3.event.clientY,
xval: this.xScale.invert(eventX), xval: this.state.xScale.invert(eventX),
yval: this.xScale.invert(eventY), yval: this.state.xScale.invert(eventY),
highlighted, highlighted,
chartX: eventX, chartX: eventX,
chartWidth: this.width chartWidth: this.width
@ -316,7 +296,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
onMouseOver(): void { onMouseOver(): void {
if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) { if(this.isPanning === true || this.isBrushing === true) {
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
return; return;
} }
@ -324,9 +304,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
onMouseOut(): void { onMouseOut(): void {
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) { this.options.callbackMouseOut();
this.options.eventsCallbacks.mouseOut();
}
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
} }
} }
@ -356,4 +334,4 @@ export const VueChartwerkScatterPodObject = {
} }
}; };
export { ScatterData, ScatterOptions, TickOrientation, TimeFormat, PointType, LineType }; export { ScatterData, ScatterOptions, TimeFormat, PointType, LineType };

13
src/delaunay.ts → src/models/delaunay.ts

@ -1,4 +1,4 @@
import { ScatterData, PointType } from './types'; import { ScatterData, PointType } from '../types';
import { Delaunay } from 'd3-delaunay'; import { Delaunay } from 'd3-delaunay';
@ -72,18 +72,9 @@ export class DelaunayDiagram {
private concatSeriesDatapoints(series: ScatterData[]): number[][] { private concatSeriesDatapoints(series: ScatterData[]): number[][] {
// return type row: [ 0:x, 1:y, 2?:custom value, last:serieIdx ] // return type row: [ 0:x, 1:y, 2?:custom value, last:serieIdx ]
const datapointsList = _.map(series, serie => { const datapointsList = _.map(series, serie => {
const serieIdx = this.getSerieIdxByTarget(serie.target); const datapointsWithOptions = _.map(serie.datapoints, row => _.concat(row, serie.idx));
const datapointsWithOptions = _.map(serie.datapoints, row => _.concat(row, serieIdx));
return datapointsWithOptions; return datapointsWithOptions;
}); });
return _.union(...datapointsList); return _.union(...datapointsList);
} }
private getSerieIdxByTarget(target: string): number {
const idx = _.findIndex(this.series, serie => serie.target === target);
if(idx === -1) {
throw new Error(`Can't find serie with target: ${target}`);
}
return idx;
}
} }

23
src/models/scatter_series.ts

@ -0,0 +1,23 @@
import { CoreSeries } from '@chartwerk/core';
import { ScatterData, PointType, LineType } from '../types';
const DEFAULT_POINT_SIZE = 4;
const SCATTER_DATA_DEFAULTS = {
pointType: PointType.CIRCLE,
lineType: LineType.NONE,
pointSize: DEFAULT_POINT_SIZE,
colorFormatter: undefined
};
export class ScatterSeries extends CoreSeries<ScatterData> {
constructor(series: ScatterData[]) {
super(series);
}
protected get defaults(): ScatterData {
return { ...this._coreDefaults, ...SCATTER_DATA_DEFAULTS };
}
}

6
src/types.ts

@ -1,4 +1,4 @@
import { TimeSerie, Options } from '@chartwerk/core'; import { Serie, Options } from '@chartwerk/core';
type ScatterDataParams = { type ScatterDataParams = {
@ -9,10 +9,8 @@ type ScatterDataParams = {
} }
type ScatterOptionsParams = { type ScatterOptionsParams = {
voronoiRadius: number; voronoiRadius: number;
circleView: boolean;
renderGrid: boolean;
} }
export type ScatterData = TimeSerie & Partial<ScatterDataParams>; export type ScatterData = Serie & Partial<ScatterDataParams>;
export type ScatterOptions = Options & Partial<ScatterOptionsParams>; export type ScatterOptions = Options & Partial<ScatterOptionsParams>;
export enum PointType { export enum PointType {

6
yarn.lock

@ -6,12 +6,12 @@ __metadata:
cacheKey: 8 cacheKey: 8
"@chartwerk/core@npm:latest": "@chartwerk/core@npm:latest":
version: 0.5.4 version: 0.6.1
resolution: "@chartwerk/core@npm:0.5.4" resolution: "@chartwerk/core@npm:0.6.1"
dependencies: dependencies:
d3: ^5.7.2 d3: ^5.7.2
lodash: ^4.14.149 lodash: ^4.14.149
checksum: 0d686409377d6880f0012db3d9c1e1910cf3660885ac13196dabe93f57eca53984cf52dcf781b2876373fe9efc7b9d0209117cbcd811c50892b1de4f38a4a37f checksum: 6c57d4d3391a656f743f8459a97102fff8d4bc6f880d1d7e0abc3070dca7eff39edf46feccc7fa0dd36ca38771a63e5045966e49e65b0763404e01fee8ad11c5
languageName: node languageName: node
linkType: hard linkType: hard

Loading…
Cancel
Save