diff --git a/client/app/scripts/charts/edge-container.js b/client/app/scripts/charts/edge-container.js index b0545329d..b8c4209b5 100644 --- a/client/app/scripts/charts/edge-container.js +++ b/client/app/scripts/charts/edge-container.js @@ -1,17 +1,17 @@ import _ from 'lodash'; -import d3 from 'd3'; import React from 'react'; import { connect } from 'react-redux'; import { Motion, spring } from 'react-motion'; import { Map as makeMap } from 'immutable'; +import { line, curveBasis } from 'd3-shape'; import Edge from './edge'; const animConfig = [80, 20]; // stiffness, damping const pointCount = 30; -const line = d3.svg.line() - .interpolate('basis') +const spline = line() + .curve(curveBasis) .x(d => d.x) .y(d => d.y); @@ -23,7 +23,7 @@ const buildPath = (points, layoutPrecision) => { if (!extracted[index]) { extracted[index] = {}; } - extracted[index][axis] = d3.round(value, layoutPrecision); + extracted[index][axis] = Math.round(value, layoutPrecision); }); return extracted; }; @@ -53,7 +53,7 @@ class EdgeContainer extends React.Component { const other = _.omit(this.props, 'points'); if (layoutPrecision === 0) { - const path = line(points.toJS()); + const path = spline(points.toJS()); return ; } @@ -62,7 +62,7 @@ class EdgeContainer extends React.Component { {(interpolated) => { // convert points to path string, because that lends itself to // JS-equality checks in the child component - const path = line(buildPath(interpolated, layoutPrecision)); + const path = spline(buildPath(interpolated, layoutPrecision)); return ; }} diff --git a/client/app/scripts/charts/node-container.js b/client/app/scripts/charts/node-container.js index 74820303d..8d513e45c 100644 --- a/client/app/scripts/charts/node-container.js +++ b/client/app/scripts/charts/node-container.js @@ -1,7 +1,6 @@ import _ from 'lodash'; import React from 'react'; import { connect } from 'react-redux'; -import d3 from 'd3'; import { Motion, spring } from 'react-motion'; import Node from './node'; @@ -20,8 +19,8 @@ class NodeContainer extends React.Component { f: spring(scaleFactor, animConfig) }}> {interpolated => { - const transform = `translate(${d3.round(interpolated.x, layoutPrecision)},` - + `${d3.round(interpolated.y, layoutPrecision)})`; + const transform = `translate(${Math.round(interpolated.x, -layoutPrecision)},` + + `${Math.round(interpolated.y, -layoutPrecision)})`; return ; }} diff --git a/client/app/scripts/charts/node-networks-overlay.js b/client/app/scripts/charts/node-networks-overlay.js index b09dcac2e..7d2b02d27 100644 --- a/client/app/scripts/charts/node-networks-overlay.js +++ b/client/app/scripts/charts/node-networks-overlay.js @@ -1,5 +1,5 @@ import React from 'react'; -import d3 from 'd3'; +import { scaleOrdinal } from 'd3-scale'; import { List as makeList } from 'immutable'; import { getNetworkColor } from '../utils/color-utils'; import { isContrastMode } from '../utils/contrast-utils'; @@ -10,7 +10,7 @@ const minBarHeight = 3; const padding = 0.05; const rx = 1; const ry = rx; -const x = d3.scale.ordinal(); +const x = scaleOrdinal(); function NodeNetworksOverlay({offset, size, stack, networks = makeList()}) { // Min size is about a quarter of the width, feels about right. diff --git a/client/app/scripts/charts/node-shape-cloud.js b/client/app/scripts/charts/node-shape-cloud.js index f77af9346..cf71e326e 100644 --- a/client/app/scripts/charts/node-shape-cloud.js +++ b/client/app/scripts/charts/node-shape-cloud.js @@ -1,5 +1,5 @@ import React from 'react'; -import d3 from 'd3'; +import { extent } from 'd3-array'; import { isContrastMode } from '../utils/contrast-utils'; @@ -15,7 +15,7 @@ function toPoint(stringPair) { function getExtents(svgPath) { const points = svgPath.split(' ').filter(s => s.length > 1).map(toPoint); - return [d3.extent(points, p => p[0]), d3.extent(points, p => p[1])]; + return [extent(points, p => p[0]), extent(points, p => p[1])]; } export default function NodeShapeCloud({highlighted, size, color}) { diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index f4f4b715a..1abfe4b5b 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -1,20 +1,19 @@ import React from 'react'; -import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; -import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; +import { line, curveCardinalClosed } from 'd3-shape'; +import { getMetricValue, getMetricColor, getClipPathDefinition } from '../utils/metric-utils.js'; +import { CANVAS_METRIC_FONT_SIZE } from '../constants/styles.js'; -const line = d3.svg.line() - .interpolate('cardinal-closed') - .tension(0.25); +const spline = line() + .curve(curveCardinalClosed.tension(0.65)); function polygon(r, sides) { const a = (Math.PI * 2) / sides; - const points = [[r, 0]]; - for (let i = 1; i < sides; i++) { - points.push([r * Math.cos(a * i), r * Math.sin(a * i)]); + const points = []; + for (let i = 0; i < sides; i++) { + points.push([r * Math.sin(a * i), -r * Math.cos(a * i)]); } return points; } @@ -23,8 +22,7 @@ function polygon(r, sides) { export default function NodeShapeHeptagon({id, highlighted, size, color, metric}) { const scaledSize = size * 1.0; const pathProps = v => ({ - d: line(polygon(scaledSize * v, 7)), - transform: 'rotate(90)' + d: spline(polygon(scaledSize * v, 7)) }); const clipId = `mask-${id}`; diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hexagon.js similarity index 76% rename from client/app/scripts/charts/node-shape-hex.js rename to client/app/scripts/charts/node-shape-hexagon.js index d7045c9ab..4e4c18d72 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hexagon.js @@ -1,13 +1,12 @@ import React from 'react'; -import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; -import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; +import { line, curveCardinalClosed } from 'd3-shape'; +import { getMetricValue, getMetricColor, getClipPathDefinition } from '../utils/metric-utils.js'; +import { CANVAS_METRIC_FONT_SIZE } from '../constants/styles.js'; -const line = d3.svg.line() - .interpolate('cardinal-closed') - .tension(0.25); +const spline = line() + .curve(curveCardinalClosed.tension(0.65)); function getWidth(h) { @@ -26,14 +25,14 @@ function getPoints(h) { [0, 0.25 * h] ]; - return line(points); + return spline(points); } -export default function NodeShapeHex({id, highlighted, size, color, metric}) { +export default function NodeShapeHexagon({id, highlighted, size, color, metric}) { const pathProps = v => ({ d: getPoints(size * v * 2), - transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})` + transform: `translate(-${size * getWidth(v)}, -${size * v})` }); const shadowSize = 0.45; diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 63511fdfa..118f5194f 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -12,7 +12,7 @@ import MatchedResults from '../components/matched-results'; import NodeShapeCircle from './node-shape-circle'; import NodeShapeStack from './node-shape-stack'; import NodeShapeRoundedSquare from './node-shape-rounded-square'; -import NodeShapeHex from './node-shape-hex'; +import NodeShapeHexagon from './node-shape-hexagon'; import NodeShapeHeptagon from './node-shape-heptagon'; import NodeShapeCloud from './node-shape-cloud'; import NodeNetworksOverlay from './node-networks-overlay'; @@ -30,7 +30,7 @@ function stackedShape(Shape) { const nodeShapes = { circle: NodeShapeCircle, - hexagon: NodeShapeHex, + hexagon: NodeShapeHexagon, heptagon: NodeShapeHeptagon, square: NodeShapeRoundedSquare, cloud: NodeShapeCloud diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index ed20156a0..5f3cdb201 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -1,11 +1,14 @@ import _ from 'lodash'; -import d3 from 'd3'; import debug from 'debug'; import React from 'react'; import { connect } from 'react-redux'; import { Map as makeMap, fromJS } from 'immutable'; import timely from 'timely'; +import { scaleThreshold, scaleLinear } from 'd3-scale'; +import { event as d3Event, select } from 'd3-selection'; +import { zoom, zoomIdentity } from 'd3-zoom'; + import { nodeAdjacenciesSelector, adjacentNodesSelector } from '../selectors/chartSelectors'; import { clickBackground } from '../actions/app-actions'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; @@ -20,7 +23,7 @@ const log = debug('scope:nodes-chart'); const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY']; // make sure circular layouts a bit denser with 3-6 nodes -const radiusDensity = d3.scale.threshold() +const radiusDensity = scaleThreshold() .domain([3, 6]) .range([2.5, 3.5, 3]); @@ -80,7 +83,7 @@ function getNodeScale(nodesCount, width, height) { const normalizedNodeSize = Math.max(MIN_NODE_SIZE, Math.min(nodeSize / Math.sqrt(nodesCount), maxNodeSize)); - return d3.scale.linear().range([0, normalizedNodeSize]); + return scaleLinear().range([0, normalizedNodeSize]); } @@ -123,11 +126,11 @@ class NodesChart extends React.Component { this.state = { edges: makeMap(), nodes: makeMap(), - nodeScale: d3.scale.linear(), + nodeScale: scaleLinear(), panTranslateX: 0, panTranslateY: 0, scale: 1, - selectedNodeScale: d3.scale.linear(), + selectedNodeScale: scaleLinear(), hasZoomed: false, height: props.height || 0, width: props.width || 0, @@ -146,12 +149,11 @@ class NodesChart extends React.Component { // wipe node states when showing different topology if (nextProps.topologyId !== this.props.topologyId) { - // re-apply cached canvas zoom/pan to d3 behavior (or set defaul values) + // re-apply cached canvas zoom/pan to d3 behavior (or set the default values) const defaultZoom = { scale: 1, panTranslateX: 0, panTranslateY: 0, hasZoomed: false }; const nextZoom = this.state.zoomCache[nextProps.topologyId] || defaultZoom; if (nextZoom) { - this.zoom.scale(nextZoom.scale); - this.zoom.translate([nextZoom.panTranslateX, nextZoom.panTranslateY]); + this.setZoom(nextZoom); } // saving previous zoom state @@ -188,17 +190,17 @@ class NodesChart extends React.Component { // distinguish pan/zoom from click this.isZooming = false; - this.zoom = d3.behavior.zoom() + this.zoom = zoom() .scaleExtent([0.1, 2]) .on('zoom', this.zoomed); - d3.select('.nodes-chart svg') - .call(this.zoom); + this.svg = select('.nodes-chart svg'); + this.svg.call(this.zoom); } componentWillUnmount() { // undoing .call(zoom) - d3.select('.nodes-chart svg') + this.svg .on('mousedown.zoom', null) .on('onwheel', null) .on('onmousewheel', null) @@ -317,8 +319,7 @@ class NodesChart extends React.Component { restoreLayout(state) { // undo any pan/zooming that might have happened - this.zoom.scale(state.scale); - this.zoom.translate([state.panTranslateX, state.panTranslateY]); + this.setZoom(state); const nodes = state.nodes.map(node => node.merge({ x: node.get('px'), @@ -361,10 +362,8 @@ class NodesChart extends React.Component { const zoomFactor = Math.min(xFactor, yFactor); let zoomScale = state.scale; - if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) { + if (this.svg && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) { zoomScale = zoomFactor; - // saving in d3's behavior cache - this.zoom.scale(zoomFactor); } return { @@ -376,18 +375,27 @@ class NodesChart extends React.Component { } zoomed() { - // debug('zoomed', d3.event.scale, d3.event.translate); + // debug('zoomed', d3Event.transform); this.isZooming = true; // dont pan while node is selected if (!this.props.selectedNodeId) { this.setState({ hasZoomed: true, - panTranslateX: d3.event.translate[0], - panTranslateY: d3.event.translate[1], - scale: d3.event.scale + panTranslateX: d3Event.transform.x, + panTranslateY: d3Event.transform.y, + scale: d3Event.transform.k }); } } + + setZoom(zoom) { + this.svg.call( + this.zoom.transform, + zoomIdentity + .scale(zoom.scale) + .translate(zoom.panTranslateX, zoom.panTranslateY) + ); + } } diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 14d5eaa74..7a0ac7b65 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -1,10 +1,10 @@ /* eslint react/jsx-no-bind: "off" */ import React from 'react'; -import d3 from 'd3'; import _ from 'lodash'; import Perf from 'react-addons-perf'; import { connect } from 'react-redux'; import { fromJS, Set as makeSet } from 'immutable'; +import { hsl } from 'd3-color'; import debug from 'debug'; const log = debug('scope:debug-panel'); @@ -328,7 +328,7 @@ class DebugToolbar extends React.Component { + style={{backgroundColor: hsl(text2degree(r), 0.5, 0.5).toString()}} /> ))} diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js index 29f39c0fc..55254ffc1 100644 --- a/client/app/scripts/components/sparkline.js +++ b/client/app/scripts/components/sparkline.js @@ -1,18 +1,20 @@ // Forked from: https://github.com/KyleAMathews/react-sparkline at commit a9d7c5203d8f240938b9f2288287aaf0478df013 import React from 'react'; -import d3 from 'd3'; +import { min as d3Min, max as d3Max, mean as d3Mean } from 'd3-array'; +import { isoParse as parseDate } from 'd3-time-format'; +import { line, curveLinear } from 'd3-shape'; +import { scaleLinear } from 'd3-scale'; import { formatMetricSvg } from '../utils/string-utils'; -const parseDate = d3.time.format.iso.parse; export default class Sparkline extends React.Component { constructor(props, context) { super(props, context); - this.x = d3.scale.linear(); - this.y = d3.scale.linear(); - this.line = d3.svg.line() + this.x = scaleLinear(); + this.y = scaleLinear(); + this.line = line() .x(d => this.x(d.date)) .y(d => this.y(d.value)); } @@ -29,7 +31,7 @@ export default class Sparkline extends React.Component { // adjust scales this.x.range([2, this.props.width - 2]); this.y.range([this.props.height - 2, 2]); - this.line.interpolate(this.props.interpolate); + this.line.curve(this.props.curve); // Convert dates into D3 dates data = data.map(d => ({ @@ -50,18 +52,18 @@ export default class Sparkline extends React.Component { this.x.domain([firstDate, lastDate]); // determine value range - const minValue = this.props.min !== undefined ? this.props.min : d3.min(data, d => d.value); + const minValue = this.props.min !== undefined ? this.props.min : d3Min(data, d => d.value); const maxValue = this.props.max !== undefined - ? Math.max(this.props.max, d3.max(data, d => d.value)) : d3.max(data, d => d.value); + ? Math.max(this.props.max, d3Max(data, d => d.value)) : d3Max(data, d => d.value); this.y.domain([minValue, maxValue]); const lastValue = data[data.length - 1].value; const lastX = this.x(lastDate); const lastY = this.y(lastValue); - const min = formatMetricSvg(d3.min(data, d => d.value), this.props); - const max = formatMetricSvg(d3.max(data, d => d.value), this.props); - const mean = formatMetricSvg(d3.mean(data, d => d.value), this.props); - const title = `Last ${d3.round((lastDate - firstDate) / 1000)} seconds, ` + + const min = formatMetricSvg(d3Min(data, d => d.value), this.props); + const max = formatMetricSvg(d3Max(data, d => d.value), this.props); + const mean = formatMetricSvg(d3Mean(data, d => d.value), this.props); + const title = `Last ${Math.round((lastDate - firstDate) / 1000)} seconds, ` + `${data.length} samples, min: ${min}, max: ${max}, mean: ${mean}`; return {title, lastX, lastY, data}; @@ -98,6 +100,6 @@ Sparkline.defaultProps = { height: 24, strokeColor: '#7d7da8', strokeWidth: '0.5px', - interpolate: 'none', + curve: curveLinear, circleDiameter: 1.75 }; diff --git a/client/app/scripts/hoc/metric-feeder.js b/client/app/scripts/hoc/metric-feeder.js index 5fd233687..9a538f4d2 100644 --- a/client/app/scripts/hoc/metric-feeder.js +++ b/client/app/scripts/hoc/metric-feeder.js @@ -1,9 +1,8 @@ import React from 'react'; -import d3 from 'd3'; +import { isoParse as parseDate } from 'd3-time-format'; import { OrderedMap } from 'immutable'; const makeOrderedMap = OrderedMap; -const parseDate = d3.time.format.iso.parse; const sortDate = (v, d) => d; const DEFAULT_TICK_INTERVAL = 1000; // DEFAULT_TICK_INTERVAL + renderTime < 1000ms const WINDOW_LENGTH = 60; diff --git a/client/app/scripts/utils/color-utils.js b/client/app/scripts/utils/color-utils.js index c67821364..c728c1088 100644 --- a/client/app/scripts/utils/color-utils.js +++ b/client/app/scripts/utils/color-utils.js @@ -1,11 +1,12 @@ -import d3 from 'd3'; +import { hsl } from 'd3-color'; +import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; const PSEUDO_COLOR = '#b1b1cb'; const hueRange = [20, 330]; // exclude red -const hueScale = d3.scale.linear().range(hueRange); -const networkColorScale = d3.scale.category10(); +const hueScale = scaleLinear().range(hueRange); +const networkColorScale = scaleOrdinal(schemeCategory10); // map hues to lightness -const lightnessScale = d3.scale.linear().domain(hueRange).range([0.5, 0.7]); +const lightnessScale = scaleLinear().domain(hueRange).range([0.5, 0.7]); const startLetterRange = 'A'.charCodeAt(); const endLetterRange = 'Z'.charCodeAt(); const letterRange = endLetterRange - startLetterRange; @@ -36,8 +37,7 @@ export function colors(text, secondText) { // reuse text2degree and feed degree to lightness scale lightness = lightnessScale(text2degree(secondText)); } - const color = d3.hsl(hue, saturation, lightness); - return color; + return hsl(hue, saturation, lightness); } export function getNeutralColor() { @@ -55,31 +55,30 @@ export function getNodeColorDark(text = '', secondText = '', isPseudo = false) { if (isPseudo) { return PSEUDO_COLOR; } - const color = d3.rgb(colors(text, secondText)); - let hsl = color.hsl(); + let color = hsl(colors(text, secondText)); // ensure darkness - if (hsl.h > 20 && hsl.h < 120) { - hsl = hsl.darker(2); + if (color.h > 20 && color.h < 120) { + color = color.darker(2); } else if (hsl.l > 0.7) { - hsl = hsl.darker(1.5); + color = color.darker(1.5); } else { - hsl = hsl.darker(1); + color = color.darker(1); } - return hsl.toString(); + return color.toString(); } export function getNetworkColor(text) { return networkColorScale(text); } -export function brightenColor(color) { - let hsl = d3.rgb(color).hsl(); +export function brightenColor(c) { + let color = hsl(c); if (hsl.l > 0.5) { - hsl = hsl.brighter(0.5); + color = color.brighter(0.5); } else { - hsl = hsl.brighter(0.8); + color = color.brighter(0.8); } - return hsl.toString(); + return color.toString(); } diff --git a/client/app/scripts/utils/data-generator-utils.js b/client/app/scripts/utils/data-generator-utils.js index e79717eeb..e75a6d83a 100644 --- a/client/app/scripts/utils/data-generator-utils.js +++ b/client/app/scripts/utils/data-generator-utils.js @@ -1,5 +1,6 @@ import _ from 'lodash'; -import d3 from 'd3'; +import { scaleLinear } from 'd3-scale'; +import { extent } from 'd3-array'; // Inspired by Lee Byron's test data generator. @@ -20,7 +21,7 @@ function bumpLayer(n, maxValue) { for (i = 0; i < n; ++i) a[i] = 0; for (i = 0; i < 5; ++i) bump(a); const values = a.map(function(d) { return Math.max(0, d * maxValue); }); - const s = d3.scale.linear().domain(d3.extent(values)).range([0, maxValue]); + const s = scaleLinear().domain(extent(values)).range([0, maxValue]); return values.map(s); } /*eslint-enable */ diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index 9eed53ad3..9ae8e0465 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import d3 from 'd3'; +import { scaleLog } from 'd3-scale'; import { formatMetricSvg } from './string-utils'; import { colors } from './color-utils'; import React from 'react'; @@ -24,7 +24,7 @@ export function getClipPathDefinition(clipId, size, height, // // loadScale(1) == 0.5; E.g. a nicely balanced system :). -const loadScale = d3.scale.log().domain([0.01, 100]).range([0, 1]); +const loadScale = scaleLog().domain([0.01, 100]).range([0, 1]); export function getMetricValue(metric, size) { diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index ee09f6cae..25727259f 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -1,10 +1,11 @@ import React from 'react'; import filesize from 'filesize'; -import d3 from 'd3'; +import { format as d3Format } from 'd3-format'; +import { isoFormat } from 'd3-time-format'; import LCP from 'lcp'; import moment from 'moment'; -const formatLargeValue = d3.format('s'); +const formatLargeValue = d3Format('s'); function renderHtml(text, unit) { @@ -69,7 +70,7 @@ function makeFormatMetric(renderFn) { export const formatMetric = makeFormatMetric(renderHtml); export const formatMetricSvg = makeFormatMetric(renderSvg); -export const formatDate = d3.time.format.iso; +export const formatDate = isoFormat; // d3.time.format.iso; const CLEAN_LABEL_REGEX = /[^A-Za-z0-9]/g; export function slugify(label) { diff --git a/client/package.json b/client/package.json index b335502a2..3d4ac528b 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "dependencies": { "babel-polyfill": "~6.16.0", "classnames": "~2.2.5", - "d3": "~3.5.5", + "d3": "~4.4.0", "dagre": "0.7.4", "debug": "~2.3.3", "filesize": "~3.3.0",