From bc4288861d570de716a5db4fd308a3698db266e6 Mon Sep 17 00:00:00 2001
From: Deniz Kusefoglu <deniz@influxdata.com>
Date: Wed, 7 Mar 2018 14:16:54 -0800
Subject: [PATCH] Add crosshair component at hovertime

---
 ui/src/shared/components/Crosshair.js   |  41 +++++++++
 ui/src/shared/components/Dygraph.js     |  57 +++++++++----
 ui/src/style/chronograf.scss            |   1 +
 ui/src/style/components/crosshairs.scss | 107 ++++++++++++++++++++++++
 4 files changed, 189 insertions(+), 17 deletions(-)
 create mode 100644 ui/src/shared/components/Crosshair.js
 create mode 100644 ui/src/style/components/crosshairs.scss

diff --git a/ui/src/shared/components/Crosshair.js b/ui/src/shared/components/Crosshair.js
new file mode 100644
index 0000000000..892d87092c
--- /dev/null
+++ b/ui/src/shared/components/Crosshair.js
@@ -0,0 +1,41 @@
+import React, {PropTypes, Component} from 'react'
+import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'shared/constants'
+
+class Crosshair extends Component {
+  render() {
+    const {
+      dygraph,
+      staticLegendHeight,
+      hoverTime,
+      handleCrosshairRef,
+    } = this.props
+    const crosshairleft = Math.round(
+      Math.max(-1000, dygraph.toDomXCoord(hoverTime)) || -1000 + 1
+    )
+    const crosshairHeight = `calc(100% - ${staticLegendHeight +
+      DYGRAPH_CONTAINER_XLABEL_MARGIN}px)`
+    return (
+      <div className="new-crosshair" ref={el => handleCrosshairRef(el)}>
+        <div
+          className="new-crosshair--crosshair"
+          style={{
+            left: crosshairleft + 1,
+            height: crosshairHeight,
+            zIndex: 1999,
+          }}
+        />
+      </div>
+    )
+  }
+}
+
+const {func, number, shape, string} = PropTypes
+
+Crosshair.propTypes = {
+  dygraph: shape({}),
+  staticLegendHeight: number,
+  hoverTime: string,
+  handleCrosshairRef: func,
+}
+
+export default Crosshair
diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js
index f3d8900613..ca8f861b14 100644
--- a/ui/src/shared/components/Dygraph.js
+++ b/ui/src/shared/components/Dygraph.js
@@ -9,6 +9,7 @@ import Dygraphs from 'src/external/dygraph'
 import DygraphLegend from 'src/shared/components/DygraphLegend'
 import StaticLegend from 'src/shared/components/StaticLegend'
 import Annotations from 'src/shared/components/Annotations'
+import Crosshair from 'src/shared/components/Crosshair'
 
 import getRange, {getStackedRange} from 'shared/parsing/getRangeForDygraph'
 import {DISPLAY_OPTIONS} from 'src/dashboards/constants'
@@ -196,10 +197,26 @@ class Dygraph extends Component {
     onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper))
   }
 
-  highlightCallback = (e, x) => {
+  clampWithinGraphTimerange = timestamp => {
+    const [xRangeStart] = this.dygraph.xAxisRange()
+    return Math.max(xRangeStart, timestamp)
+  }
+
+  eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => {
+    const {
+      left: pxBetweenGraphAndPage,
+    } = this.crosshairRef.getBoundingClientRect()
+    const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage
+    const timestamp = this.dygraph.toDataXCoord(graphXCoordinate)
+    const clamped = this.clampWithinGraphTimerange(timestamp)
+    return `${clamped}`
+  }
+
+  highlightCallback = e => {
     const {onSetHoverTime} = this.props
+    const newTime = this.eventToTimestamp(e)
     if (onSetHoverTime) {
-      onSetHoverTime(x.toString())
+      onSetHoverTime(newTime)
     }
   }
 
@@ -311,6 +328,7 @@ class Dygraph extends Component {
   }
 
   handleAnnotationsRef = ref => (this.annotationsRef = ref)
+  handleCrosshairRef = ref => (this.crosshairRef = ref)
 
   handleReceiveStaticLegendHeight = staticLegendHeight => {
     this.setState({staticLegendHeight})
@@ -318,8 +336,7 @@ class Dygraph extends Component {
 
   render() {
     const {isHidden, staticLegendHeight} = this.state
-    const {staticLegend, children} = this.props
-
+    const {staticLegend, children, hoverTime} = this.props
     const nestedGraph = (children && children.length && children[0]) || children
     let dygraphStyle = {...this.props.containerStyle, zIndex: '2'}
     if (staticLegend) {
@@ -331,22 +348,28 @@ class Dygraph extends Component {
         height: `calc(100% - ${staticLegendHeight + cellVerticalPadding}px)`,
       }
     }
-
     return (
       <div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
         {this.dygraph &&
-          <Annotations
-            dygraph={this.dygraph}
-            annotationsRef={this.handleAnnotationsRef}
-            staticLegendHeight={staticLegendHeight}
-          />}
-        {this.dygraph &&
-          <DygraphLegend
-            isHidden={isHidden}
-            dygraph={this.dygraph}
-            onHide={this.handleHideLegend}
-            onShow={this.handleShowLegend}
-          />}
+          <div>
+            <Annotations
+              dygraph={this.dygraph}
+              annotationsRef={this.handleAnnotationsRef}
+              staticLegendHeight={staticLegendHeight}
+            />
+            <DygraphLegend
+              isHidden={isHidden}
+              dygraph={this.dygraph}
+              onHide={this.handleHideLegend}
+              onShow={this.handleShowLegend}
+            />
+            <Crosshair
+              dygraph={this.dygraph}
+              staticLegendHeight={staticLegendHeight}
+              hoverTime={hoverTime}
+              handleCrosshairRef={this.handleCrosshairRef}
+            />
+          </div>}
         <div
           ref={r => {
             this.graphRef = r
diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss
index 9c7c0d73b0..562d273a40 100644
--- a/ui/src/style/chronograf.scss
+++ b/ui/src/style/chronograf.scss
@@ -33,6 +33,7 @@
 
 // Components
 @import 'components/annotations';
+@import 'components/crosshairs';
 @import 'components/ceo-display-options';
 @import 'components/confirm-button';
 @import 'components/confirm-buttons';
diff --git a/ui/src/style/components/crosshairs.scss b/ui/src/style/components/crosshairs.scss
new file mode 100644
index 0000000000..c5eb1837ec
--- /dev/null
+++ b/ui/src/style/components/crosshairs.scss
@@ -0,0 +1,107 @@
+$crosshair-color: $g20-white;
+$crosshair-color__drag: $c-hydrogen;
+
+$window0: rgba($crosshair-color, 0);
+$window15: rgba($crosshair-color, 0.15);
+$window35: rgba($crosshair-color, 0.35);
+
+$active-window0: rgba($crosshair-color__drag, 0);
+$active-window15: rgba($crosshair-color__drag, 0.15);
+$active-window35: rgba($crosshair-color__drag, 0.35);
+
+$timestamp-font-size: 14px;
+$timestamp-font-weight: 600;
+
+.crosshair {
+  position: absolute;
+  top: 8px;
+  z-index: 3;
+  background-color: $crosshair-color;
+  height: calc(100% - 36px);
+  width: 2px;
+  transform: translateX(
+    -1px
+  ); // translate should always be half with width to horizontally center the crosshair pos
+  transition: background-color 0.25s ease;
+  visibility: visible;
+
+  &.dragging {
+    background-color: $crosshair-color__drag;
+    z-index: 4;
+  }
+}
+
+.crosshair-window {
+  position: absolute;
+  top: 8px;
+  background: linear-gradient(to bottom, $window15 0%, $window0 100%);
+  border-top: 2px dotted $window35;
+  z-index: 1;
+
+  &.active {
+    background: linear-gradient(
+      to bottom,
+      $active-window15 0%,
+      $active-window0 100%
+    );
+    border-top: 2px dotted $active-window35;
+  }
+}
+
+/*
+  New crosshairs
+  ------------------------------------------------------------------------------
+*/
+.new-crosshair {
+  position: absolute;
+  z-index: 0;
+  top: 8px;
+  width: calc(100% - 32px);
+  height: calc(100% - 16px);
+  cursor: pointer;
+  opacity: 1;
+}
+
+.new-crosshair--crosshair {
+  opacity: 1;
+  position: absolute;
+  top: 0;
+  height: calc(100% - 20px);
+  width: 0.5px;
+  transform: translateX(-1px);
+  background: linear-gradient(to bottom, $c-dreamsicle 0%, $c-dreamsicle 100%);
+  transition: opacity 0.4s ease;
+  z-index: 5;
+  cursor: pointer;
+}
+
+.new-crosshair-tooltip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background: linear-gradient(to bottom, $c-pool 0%, $c-ocean 100%);
+  border-radius: 4px;
+  padding: 6px 12px;
+  position: absolute;
+  bottom: calc(100% + 8px);
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 10;
+}
+
+.new-crosshair-helper {
+  white-space: nowrap;
+  font-size: 13px;
+  line-height: 13px;
+  font-weight: 600;
+  color: $c-neutrino;
+  margin-bottom: 4px;
+}
+
+.new-crosshair-timestamp {
+  white-space: nowrap;
+  font-size: $timestamp-font-size;
+  line-height: $timestamp-font-size;
+  font-weight: $timestamp-font-weight;
+  color: $g20-white;
+}