]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9416 Fix and improve bubble chart zoom
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Tue, 29 May 2018 09:03:00 +0000 (11:03 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 5 Jun 2018 18:20:51 +0000 (20:20 +0200)
server/sonar-web/package.json
server/sonar-web/src/main/js/components/charts/BubbleChart.css
server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
server/sonar-web/yarn.lock

index 02b243b8aea7581aee311ab383c69d254385de53..241641cf9047dd35edefb5dc477a27fbe1d40c9a 100644 (file)
@@ -14,6 +14,7 @@
     "d3-scale": "2.0.0",
     "d3-selection": "1.3.0",
     "d3-shape": "1.2.0",
+    "d3-zoom": "1.7.1",
     "date-fns": "1.29.0",
     "formik": "0.11.11",
     "history": "3.3.0",
@@ -43,7 +44,9 @@
     "@types/d3-array": "1.2.1",
     "@types/d3-hierarchy": "1.1.1",
     "@types/d3-scale": "2.0.0",
+    "@types/d3-selection": "1.3.0",
     "@types/d3-shape": "1.2.2",
+    "@types/d3-zoom": "1.7.1",
     "@types/enzyme": "3.1.10",
     "@types/jest": "22.2.3",
     "@types/keymaster": "1.6.28",
index 9bcecc465eb581849c292b8f6c29f0b3fff06d0c..2f5c872a69b1f571b44a84f1a971f2f8ef01c5f0 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-.bubble-chart.is-moving {
-  cursor: move;
-}
-
 .bubble-chart text {
   user-select: none;
 }
index ec134895fd3a449504080b2f1debe71aaa268c49..f636a57ee145afd68f7b698687a9a8f83db8b715 100644 (file)
@@ -23,6 +23,8 @@ import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
 import { Link } from 'react-router';
 import { min, max } from 'd3-array';
 import { scaleLinear, ScaleLinear } from 'd3-scale';
+import { zoom, zoomIdentity } from 'd3-zoom';
+import { event, select } from 'd3-selection';
 import { sortBy, uniq } from 'lodash';
 import Tooltip from '../controls/Tooltip';
 import { translate } from '../../helpers/l10n';
@@ -35,6 +37,7 @@ interface BubbleProps {
   link?: string;
   onClick?: (link?: string) => void;
   r: number;
+  scale: number;
   tooltip?: string | React.ReactNode;
   x: number;
   y: number;
@@ -56,7 +59,7 @@ export class Bubble extends React.PureComponent<BubbleProps> {
         onClick={this.props.onClick ? this.handleClick : undefined}
         r={this.props.r}
         style={{ fill: this.props.color, stroke: this.props.color }}
-        transform={`translate(${this.props.x}, ${this.props.y})`}
+        transform={`translate(${this.props.x}, ${this.props.y}) scale(${this.props.scale})`}
       />
     );
 
@@ -100,16 +103,16 @@ interface Props {
 }
 
 interface State {
-  isMoving: boolean;
-  moveOrigin: { x: number; y: number };
-  zoom: number;
-  zoomOrigin: { x: number; y: number };
+  transform: { x: number; y: number; k: number };
 }
 
 type Scale = ScaleLinear<number, number>;
 
 export default class BubbleChart extends React.Component<Props, State> {
   node: SVGSVGElement | null = null;
+  selection: any = null;
+  transform: any = null;
+  zoom: any = null;
 
   static defaultProps = {
     displayXGrid: true,
@@ -121,23 +124,33 @@ export default class BubbleChart extends React.Component<Props, State> {
 
   constructor(props: Props) {
     super(props);
-    this.state = {
-      isMoving: false,
-      moveOrigin: { x: 0, y: 0 },
-      zoom: 1,
-      zoomOrigin: { x: 0, y: 0 }
-    };
+    this.state = { transform: { x: 0, y: 0, k: 1 } };
   }
 
-  componentDidMount() {
-    document.addEventListener('mouseup', this.stopMoving);
-    document.addEventListener('mousemove', this.updateZoomCenter);
+  componentDidUpdate() {
+    if (this.zoom && this.node) {
+      const rect = this.node.getBoundingClientRect();
+      this.zoom.translateExtent([[0, 0], [rect.width, rect.height]]);
+    }
   }
 
-  componentWillUnmount() {
-    document.removeEventListener('mouseup', this.stopMoving);
-    document.removeEventListener('mousemove', this.updateZoomCenter);
-  }
+  boundNode = (node: SVGSVGElement) => {
+    this.node = node;
+    this.zoom = zoom()
+      .scaleExtent([1, 10])
+      .on('zoom', this.zoomed);
+    this.selection = select(this.node).call(this.zoom);
+  };
+
+  zoomed = () => {
+    this.setState({ transform: event.transform });
+  };
+
+  resetZoom = (event: React.MouseEvent<Link>) => {
+    event.stopPropagation();
+    event.preventDefault();
+    select(this.node).call(this.zoom.transform, zoomIdentity);
+  };
 
   get formatXTick() {
     return this.props.formatXTick || ((d: number) => String(d));
@@ -151,92 +164,6 @@ export default class BubbleChart extends React.Component<Props, State> {
     return this.props.padding || [10, 10, 10, 10];
   }
 
-  startMoving = (event: React.MouseEvent<SVGSVGElement>) => {
-    if (this.node && this.state.zoom > 1) {
-      const rect = this.node.getBoundingClientRect();
-      this.setState({
-        isMoving: true,
-        moveOrigin: { x: event.clientX - rect.left, y: event.clientY - rect.top }
-      });
-    }
-  };
-
-  updateZoomCenter = (event: MouseEvent) => {
-    if (this.node && this.state.isMoving) {
-      const rect = this.node.getBoundingClientRect();
-      const x = event.clientX - rect.left;
-      const y = event.clientY - rect.top;
-      this.setState(state => ({
-        zoomOrigin: {
-          x: Math.max(-100, state.zoomOrigin.x + (state.moveOrigin.x - x) / state.zoom),
-          y: Math.max(-100, state.zoomOrigin.y + (state.moveOrigin.y - y) / state.zoom)
-        },
-        moveOrigin: { x, y }
-      }));
-    }
-  };
-
-  stopMoving = () => {
-    this.setState({ isMoving: false });
-  };
-
-  onWheel = (event: React.WheelEvent<SVGSVGElement>) => {
-    if (this.node) {
-      event.stopPropagation();
-      event.preventDefault();
-
-      const rect = this.node.getBoundingClientRect();
-      const mouseX = event.clientX - rect.left - this.padding[1];
-      const mouseY = event.clientY - rect.top - this.padding[0];
-
-      let delta = event.deltaY;
-      if ((event as any).webkitDirectionInvertedFromDevice) {
-        delta = -delta;
-      }
-
-      if (delta > 0) {
-        this.handleZoomOut(mouseX, mouseY);
-      } else {
-        this.handleZoomIn(mouseX, mouseY);
-      }
-    }
-  };
-
-  handleZoomOut = (x: number, y: number) => {
-    if (this.state.zoom === 1) {
-      this.setState(state => ({
-        zoom: Math.max(1.0, state.zoom - 0.5),
-        zoomOrigin: { x, y }
-      }));
-    } else {
-      this.setState(state => ({
-        zoom: Math.max(1.0, state.zoom - 0.5)
-      }));
-    }
-  };
-
-  handleZoomIn = (x: number, y: number) => {
-    if (this.state.zoom === 1) {
-      this.setState(state => ({
-        zoom: Math.min(10.0, state.zoom + 0.5),
-        zoomOrigin: { x, y }
-      }));
-    } else {
-      this.setState(state => ({
-        zoom: Math.min(10.0, state.zoom + 0.5)
-      }));
-    }
-  };
-
-  resetZoom = (event: React.MouseEvent<Link>) => {
-    event.stopPropagation();
-    event.preventDefault();
-    this.setState({
-      zoom: 1,
-      zoomOrigin: { x: 0, y: 0 }
-    });
-  };
-
   getXRange(xScale: Scale, sizeScale: Scale, availableWidth: number) {
     const minX = min(this.props.items, d => xScale(d.x) - sizeScale(d.size)) || 0;
     const maxX = max(this.props.items, d => xScale(d.x) + sizeScale(d.size)) || 0;
@@ -254,19 +181,22 @@ export default class BubbleChart extends React.Component<Props, State> {
   }
 
   getTicks(scale: Scale, format: (d: number) => string) {
-    const ticks = scale.ticks(TICKS_COUNT).map(tick => format(tick));
+    const zoom = Math.ceil(this.state.transform.k);
+    const ticks = scale.ticks(TICKS_COUNT * zoom).map(tick => format(tick));
     const uniqueTicksCount = uniq(ticks).length;
-    const ticksCount = uniqueTicksCount < TICKS_COUNT ? uniqueTicksCount - 1 : TICKS_COUNT;
+    const ticksCount =
+      uniqueTicksCount < TICKS_COUNT * zoom ? uniqueTicksCount - 1 : TICKS_COUNT * zoom;
     return scale.ticks(ticksCount);
   }
 
-  getZoomLevelLabel = () => this.state.zoom * 100 + '%';
+  getZoomLevelLabel = () => Math.floor(this.state.transform.k * 100) + '%';
 
-  renderXGrid = (ticks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+  renderXGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
     if (!this.props.displayXGrid) {
       return null;
     }
 
+    const { transform } = this.state;
     const lines = ticks.map((tick, index) => {
       const x = xScale(tick);
       const y1 = yScale.range()[0];
@@ -275,10 +205,10 @@ export default class BubbleChart extends React.Component<Props, State> {
         <line
           className="bubble-chart-grid"
           key={index}
-          x1={x * this.state.zoom - delta}
-          x2={x * this.state.zoom - delta}
-          y1={y1}
-          y2={y2}
+          x1={x * transform.k + transform.x}
+          x2={x * transform.k + transform.x}
+          y1={y1 * transform.k}
+          y2={transform.k > 1 ? 0 : y2}
         />
       );
     });
@@ -286,11 +216,12 @@ export default class BubbleChart extends React.Component<Props, State> {
     return <g>{lines}</g>;
   };
 
-  renderYGrid = (ticks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+  renderYGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
     if (!this.props.displayYGrid) {
       return null;
     }
 
+    const { transform } = this.state;
     const lines = ticks.map((tick, index) => {
       const y = yScale(tick);
       const x1 = xScale.range()[0];
@@ -299,10 +230,10 @@ export default class BubbleChart extends React.Component<Props, State> {
         <line
           className="bubble-chart-grid"
           key={index}
-          x1={x1}
-          x2={x2}
-          y1={y * this.state.zoom - delta}
-          y2={y * this.state.zoom - delta}
+          x1={transform.k > 1 ? 0 : x1}
+          x2={x2 * transform.k}
+          y1={y * transform.k + transform.y}
+          y2={y * transform.k + transform.y}
         />
       );
     });
@@ -310,56 +241,54 @@ export default class BubbleChart extends React.Component<Props, State> {
     return <g>{lines}</g>;
   };
 
-  renderXTicks = (xTicks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+  renderXTicks = (xTicks: number[], xScale: Scale, yScale: Scale) => {
     if (!this.props.displayXTicks) {
       return null;
     }
 
+    const { transform } = this.state;
     const ticks = xTicks.map((tick, index) => {
-      const x = xScale(tick);
+      const x = xScale(tick) * transform.k + transform.x;
       const y = yScale.range()[0];
       const innerText = this.formatXTick(tick);
-      return (
-        <text
-          className="bubble-chart-tick"
-          dy="1.5em"
-          key={index}
-          x={x * this.state.zoom - delta}
-          y={y}>
+      return x > 0 ? (
+        <text className="bubble-chart-tick" dy="1.5em" key={index} x={x} y={y}>
           {innerText}
         </text>
-      );
+      ) : null;
     });
 
     return <g>{ticks}</g>;
   };
 
-  renderYTicks = (yTicks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+  renderYTicks = (yTicks: number[], xScale: Scale, yScale: Scale) => {
     if (!this.props.displayYTicks) {
       return null;
     }
 
+    const { transform } = this.state;
     const ticks = yTicks.map((tick, index) => {
       const x = xScale.range()[0];
-      const y = yScale(tick);
+      const y = yScale(tick) * transform.k + transform.y;
       const innerText = this.formatYTick(tick);
-      return (
+      return y > 0 && y < this.props.height - 80 ? (
         <text
           className="bubble-chart-tick bubble-chart-tick-y"
           dx="-0.5em"
           dy="0.3em"
           key={index}
           x={x}
-          y={y * this.state.zoom - delta}>
+          y={y}>
           {innerText}
         </text>
-      );
+      ) : null;
     });
 
     return <g>{ticks}</g>;
   };
 
   renderChart = (width: number) => {
+    const { transform } = this.state;
     const availableWidth = width - this.padding[1] - this.padding[3];
     const availableHeight = this.props.height - this.padding[0] - this.padding[2];
 
@@ -381,9 +310,6 @@ export default class BubbleChart extends React.Component<Props, State> {
     xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
     yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
 
-    const centerXDelta = this.state.zoomOrigin.x * this.state.zoom - this.state.zoomOrigin.x;
-    const centerYDelta = this.state.zoomOrigin.y * this.state.zoom - this.state.zoomOrigin.y;
-
     const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
       return (
         <Bubble
@@ -392,9 +318,10 @@ export default class BubbleChart extends React.Component<Props, State> {
           link={item.link}
           onClick={this.props.onBubbleClick}
           r={sizeScale(item.size)}
+          scale={1 / transform.k}
           tooltip={item.tooltip}
-          x={xScale(item.x) * this.state.zoom - centerXDelta}
-          y={yScale(item.y) * this.state.zoom - centerYDelta}
+          x={xScale(item.x)}
+          y={yScale(item.y)}
         />
       );
     });
@@ -404,23 +331,31 @@ export default class BubbleChart extends React.Component<Props, State> {
 
     return (
       <svg
-        className={classNames('bubble-chart', { 'is-moving': this.state.isMoving })}
+        className={classNames('bubble-chart')}
         height={this.props.height}
-        onMouseDown={this.startMoving}
-        onWheel={this.onWheel}
-        ref={node => (this.node = node)}
+        ref={this.boundNode}
         width={width}>
+        <defs>
+          <clipPath id="graph-region">
+            <rect
+              // Extend clip by 2 pixels: one for clipRect border, and one for Bubble borders
+              height={this.props.height - this.padding[0] - this.padding[2] + 4}
+              width={width + 4}
+              x={-2}
+              y={-2}
+            />
+          </clipPath>
+        </defs>
         <g transform={`translate(${this.padding[3]}, ${this.padding[0]})`}>
-          <svg
-            height={this.props.height - this.padding[0] - this.padding[2]}
-            style={{ overflow: 'hidden' }}
-            width={width}>
-            {this.renderXGrid(xTicks, xScale, yScale, centerXDelta)}
-            {this.renderYGrid(yTicks, xScale, yScale, centerYDelta)}
-            {bubbles}
-          </svg>
-          {this.renderXTicks(xTicks, xScale, yScaleOriginal, centerXDelta)}
-          {this.renderYTicks(yTicks, xScaleOriginal, yScale, centerYDelta)}
+          <g clipPath="url(#graph-region)">
+            {this.renderXGrid(xTicks, xScale, yScale)}
+            {this.renderYGrid(yTicks, xScale, yScale)}
+            <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>
+              {bubbles}
+            </g>
+          </g>
+          {this.renderXTicks(xTicks, xScale, yScaleOriginal)}
+          {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
         </g>
       </svg>
     );
@@ -432,7 +367,7 @@ export default class BubbleChart extends React.Component<Props, State> {
         <div className="bubble-chart-zoom">
           <Tooltip overlay={translate('component_measures.bubble_chart.zoom_level')}>
             <Link
-              className={classNames('outline-badge', { active: this.state.zoom > 1 })}
+              className={classNames('outline-badge', { active: this.state.transform.k > 1 })}
               onClick={this.resetZoom}
               to="#">
               {this.getZoomLevelLabel()}
index fbbd76666bd80f263f9f02dfa2ef066e8aa0bd91..89b36a4c5ea58cfe8484990bfecb8b08cd16e6a7 100644 (file)
@@ -4,6 +4,7 @@ exports[`should display bubbles 1`] = `
 <Bubble
   key="0"
   r={45}
+  scale={1}
   x={-10}
   y={52.3015873015873}
 >
@@ -18,7 +19,7 @@ exports[`should display bubbles 1`] = `
             "stroke": undefined,
           }
         }
-        transform="translate(-10, 52.3015873015873)"
+        transform="translate(-10, 52.3015873015873) scale(1)"
       />
     </g>
   </Tooltip>
@@ -29,6 +30,7 @@ exports[`should display bubbles 2`] = `
 <Bubble
   key="1"
   r={33.57142857142857}
+  scale={1}
   x={-75}
   y={33.57142857142857}
 >
@@ -43,7 +45,7 @@ exports[`should display bubbles 2`] = `
             "stroke": undefined,
           }
         }
-        transform="translate(-75, 33.57142857142857)"
+        transform="translate(-75, 33.57142857142857) scale(1)"
       />
     </g>
   </Tooltip>
@@ -55,6 +57,7 @@ exports[`should render bubble links 1`] = `
   key="0"
   link="foo"
   r={45}
+  scale={1}
   x={-10}
   y={52.3015873015873}
 >
@@ -78,7 +81,7 @@ exports[`should render bubble links 1`] = `
                 "stroke": undefined,
               }
             }
-            transform="translate(-10, 52.3015873015873)"
+            transform="translate(-10, 52.3015873015873) scale(1)"
           />
         </a>
       </Link>
@@ -92,6 +95,7 @@ exports[`should render bubble links 2`] = `
   key="1"
   link="bar"
   r={33.57142857142857}
+  scale={1}
   x={-75}
   y={33.57142857142857}
 >
@@ -115,7 +119,7 @@ exports[`should render bubble links 2`] = `
                 "stroke": undefined,
               }
             }
-            transform="translate(-75, 33.57142857142857)"
+            transform="translate(-75, 33.57142857142857) scale(1)"
           />
         </a>
       </Link>
@@ -130,6 +134,7 @@ exports[`should render bubbles with click handlers 1`] = `
   link="foo"
   onClick={[MockFunction]}
   r={45}
+  scale={1}
   x={-10}
   y={52.3015873015873}
 >
@@ -145,7 +150,7 @@ exports[`should render bubbles with click handlers 1`] = `
             "stroke": undefined,
           }
         }
-        transform="translate(-10, 52.3015873015873)"
+        transform="translate(-10, 52.3015873015873) scale(1)"
       />
     </g>
   </Tooltip>
@@ -158,6 +163,7 @@ exports[`should render bubbles with click handlers 2`] = `
   link="bar"
   onClick={[MockFunction]}
   r={33.57142857142857}
+  scale={1}
   x={-75}
   y={33.57142857142857}
 >
@@ -173,7 +179,7 @@ exports[`should render bubbles with click handlers 2`] = `
             "stroke": undefined,
           }
         }
-        transform="translate(-75, 33.57142857142857)"
+        transform="translate(-75, 33.57142857142857) scale(1)"
       />
     </g>
   </Tooltip>
index 91927389191139612be46686421986123c314ee5..e99a7491fcfc936511f202496eaf441c085c2fd2 100644 (file)
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.1.tgz#e489605208d46a1c9d980d2e5772fa9c75d9ec65"
 
+"@types/d3-color@*":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.1.tgz#26141c3c554e320edd40726b793570a3ae57397e"
+
 "@types/d3-hierarchy@1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.1.tgz#f1b7f4bab0a9ecfb9c372a303ded97d83db3cbbf"
 
+"@types/d3-interpolate@*":
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.1.6.tgz#64041b15c9c032c348da1b22baabc59fa4d16136"
+  dependencies:
+    "@types/d3-color" "*"
+
 "@types/d3-path@*":
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.6.tgz#c1a7d2dc07b295fdd1c84dabe4404df991b48693"
   dependencies:
     "@types/d3-time" "*"
 
+"@types/d3-selection@*":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.1.tgz#c6227f4e39d429cc429ce3882fd533facc7f014c"
+
+"@types/d3-selection@1.3.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.0.tgz#acede3d22c18ec085cc401d4fdab9f040e1a73c7"
+
 "@types/d3-shape@1.2.2":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.2.tgz#f8dcdff7772a7ae37858bf04abd43848a78e590e"
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.7.tgz#4266d7c9be15fa81256a88d1d052d61cd8dc572c"
 
+"@types/d3-zoom@1.7.1":
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.1.tgz#0d69be0bf5849cffb66f48e4258c838436b43822"
+  dependencies:
+    "@types/d3-interpolate" "*"
+    "@types/d3-selection" "*"
+
 "@types/enzyme@3.1.10":
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.10.tgz#28108a9864e65699751469551a803a35d2e26160"
@@ -2320,6 +2345,21 @@ d3-color@1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
 
+d3-dispatch@1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
+
+d3-drag@1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
+  dependencies:
+    d3-dispatch "1"
+    d3-selection "1"
+
+d3-ease@1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
+
 d3-format@1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.0.tgz#6b480baa886885d4651dc248a8f4ac9da16db07a"
@@ -2349,7 +2389,7 @@ d3-scale@2.0.0:
     d3-time "1"
     d3-time-format "2"
 
-d3-selection@1.3.0:
+d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
 
@@ -2369,6 +2409,31 @@ d3-time@1:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.7.tgz#94caf6edbb7879bb809d0d1f7572bc48482f7270"
 
+d3-timer@1:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
+
+d3-transition@1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
+  dependencies:
+    d3-color "1"
+    d3-dispatch "1"
+    d3-ease "1"
+    d3-interpolate "1"
+    d3-selection "^1.1.0"
+    d3-timer "1"
+
+d3-zoom@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
 damerau-levenshtein@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"