Displaying a vertical line (rule) on mouseover / mousemove

974 views
Skip to first unread message

Calvin Young

unread,
Oct 10, 2016, 8:51:14 PM10/10/16
to vega-js
I'm trying to implement a line chart with a vertical rule that appears when the user mouses into the graph. My current approach is as follows (the full spec below):
  1. Add a mousemove / mouseout signal that returns the eventX() value on mousemove, and resets to {} on mouseout:
    {
      "name": "signal_date",
      "init": {},
      "streams": [
        {
          "type": "mousemove",
          "expr": "clamp(eventX(), 0, eventGroup('root').width)",
          "scale": {"name": "scale_x","invert": true}
        },
        {"type": "mouseout","expr": "{}"}
      ]
    }

  2. Use a production rule to set fillOpacity=0 if signal_date is empty:
    "fillOpacity": [
      {
        "test": "if(signal_date.length > 0, true, false)",
        "value": 0
      },
      {"value": 1}
    ]

However, this doesn't work. Specifically, the following happens:
  1. The vertical rule is always visible, and resets back to the x=0 position on mouseout.
  2. The mousemove event is triggered when the mouse enters anywhere in the visualization (including outside the axes). Ideally, I'd like the vertical rule to appear only when the cursor is inside the graph (i.e., inside the graph axes).
This seems like it ought to be fairly straightforward — what am I missing?

----------------------------------------------------------------------------

Full spec:

{
  "data": [
    {
      "name": "table",
      "values": [
        {"date": "2016-08-07","value": 28},
        {"date": "2016-08-14","value": 55},
        {"date": "2016-08-21","value": 18},
        {"date": "2016-08-28","value": 24},
        {"date": "2016-09-04","value": 49}
      ],
      "transform": [
        {
          "type": "formula",
          "field": "date",
          "expr": "datetime(datum.date)"
        }
      ]
    }
  ],
  "signals": [
    {
      "name": "signal_date",
      "init": {},
      "streams": [
        {
          "type": "mousemove",
          "expr": "clamp(eventX(), 0, eventGroup('root').width)",
          "scale": {"name": "scale_x","invert": true}
        },
        {"type": "mouseout","expr": "{}"}
      ]
    }
  ],
  "scales": [
    {
      "name": "scale_x",
      "type": "time",
      "range": "width",
      "domain": {"data": "table","field": "date"}
    },
    {
      "name": "scale_y",
      "type": "linear",
      "range": "height",
      "domain": {"data": "table","field": "value"},
      "nice": true
    }
  ],
  "axes": [
    {
      "type": "x",
      "scale": "scale_x",
      "title": "Date",
      "ticks": 5
    },
    {"type": "y","scale": "scale_y","title": "Value"}
  ],
  "marks": [
    {
      "type": "line",
      "from": {"data": "table"},
      "properties": {
        "update": {
          "x": {"scale": "scale_x","field": "date"},
          "y": {"scale": "scale_y","field": "value"},
          "stroke": {"value": "red"}
        }
      }
    },
    {
      "type": "rule",
      "properties": {
        "update": {
          "x": {"scale": "scale_x","signal": "signal_date"},
          "y": {"value": 0},
          "y2": {"field": {"group": "height"}},
          "stroke": {"value": "green"},
          "fillOpacity": [
            {
              "test": "if(signal_date.length > 0, true, false)",
              "value": 0
            },
            {"value": 1}
          ]
        }
      }
    }
  ]
}

Calvin Young

unread,
Oct 11, 2016, 9:01:21 PM10/11/16
to vega-js
For reference, I was able to figure out how to make sure the vertical rule disappears on mouseout. The key was to do the following:

1) Define the signal with an initial value of `null`, and reset it to `null` on mouseout:
{
  name: "signal_date",
  init: null,
  streams: [
    {
      type: "mousemove",
      expr: "clamp(eventX(), 0, eventGroup('root').width)",
      scale: {
        name: "scale_x",
        invert: true
      }
    },
    {
      type: "mouseout",
      expr: "null"
    }
  ]
}

2) Use the following production rule for the x-coordinate of the vertical rule (this is very important to make sure vega doesn't crash when trying to set an x-coordinate of `null`):
x: [
  {
    test: "signal_date !== null",
    scale: "scale_x",
    signal: "signal_date"
  },
  {
    value: 0
  }
]

3) Use a production rule to set the `strokeOpacity` to 0 when the signal is `null` (I was originally using `fillOpacity`, which was an oversight):
strokeOpacity: [
  {
    test: "signal_date === null",
    value: 0
  },
  {
    value: 1
  }
]

Hope that helps someone else at some point...

Roy I

unread,
Oct 12, 2016, 9:17:26 AM10/12/16
to vega-js
Here is a working version with several modifications including:

- Use a mark rect to define the plot area

- signal_date defined for "@plot_area:mousemove" and value is "Invalid Date" if outside of plot area

- Added mark text to show the value of signal_date for debug

Notes: 

1. There seems to be a glitch: when moving the mouse slightly in vertical direction, the signal_data value can become invalid (not sure why).

2. In general, to avoid unexpected behavior, define transform formula fields with new unique names (i.e. not name them the same as data field names)  

3. For your information: Javascript datetime can be confusing (UTC vs local time).
Datetime format "2016-08-07" is parsed as UTC (midnight), while "2016/08/07" is parsed as local time (midnight).
Vega x-axis and signal_date will display datetime as local time.
So data with datetime parsed as UTC may appear in Vega chart to be offset by several hours depending on your time zone (may appear as a different day)
See: "JavaScript and Dates, What a Mess!"



Vega spec (v2.6.3)
--------------------------
{
  "data": [
    {
      "name": "table",
      "values": [
        {"dateYMD": "2016-08-07", "value": 28},
        {"dateYMD": "2016-08-14", "value": 55},
        {"dateYMD": "2016-08-21", "value": 18},
        {"dateYMD": "2016-08-28", "value": 24},
        {"dateYMD": "2016-09-04", "value": 49}
      ],
      "transform": [
        {
          "type": "formula",
          "field": "ff_date",
          "expr": "datetime(datum.dateYMD)"
        }
      ]
    }
  ],
  "signals": [
    {
      "name": "signal_date",
     "init": {"expr": "datetime('')"},  
      "streams": [
{
         "type": "@plot_area:mousemove",
          "scale": {"name": "scale_x", "invert": true},
 "expr": "eventX()"
 
        },
        {
"type": "@plot_area:mouseout", 
"expr": "datetime('')"
}
      ]
    }
  ],
  "scales": [
    {
      "name": "scale_x",
      "type": "time",
      "range": "width",
      "domain": {"data": "table", "field": "ff_date"}
    },
    {
      "name": "scale_y",
      "type": "linear",
      "range": "height",
      "domain": {"data": "table", "field": "value"},
      "nice": true
    }
  ],
  "axes": [
    {
      "type": "x",
      "scale": "scale_x",
      "title": "Date",
      "ticks": 5
    },
    {"type": "y", "scale": "scale_y", "title": "Value"}
  ],
  "marks": [
  
   {
      "type": "rect",
 "name": "plot_area",
      "properties": {
        "enter": {
          "x": {"value": 0},
 "width": {"signal": "width"},
          "y": {"value": 0},
 "height": {"signal": "height"},
          "fill": {"value": "beige"},
 "fillOpacity": {"value": 0.5}
        }
      }
    },
  
  
    {
      "type": "line",
      "from": {"data": "table"},
      "properties": {
        "update": {
          "x": {"scale": "scale_x", "field": "ff_date"},
 
          "y": {"scale": "scale_y", "field": "value"},
          "stroke": {"value": "red"}
        }
      }
    },
    {
      "type": "rule",
      "properties": {
        "update": {
          "x": {"scale": "scale_x", "signal": "signal_date"},
          "y": {"value": 0},
 "y2": {"field": {"group": "height"}},
 
          "stroke": [
{
              "test": "signal_date < datetime('2016-08-21')",
              "value": "green"
            },
{"value": "magenta"}
 ],
 
          "strokeOpacity": [
            {
              "test": "signal_date == 'Invalid Date'",
              "value": 0.0
            },
            {"value": 1.0}
          ]
 
        }
      }
    },
  {
      "type": "text",
      "properties": {
        "enter": {
          "x": {"value": 150},
          "y": {"value": 25},
          "fontSize": {"value": 10},
          "fill": {"value": "blue"},
 "fillOpacity": {"value": 1.0}
        },
        "update": {
          "text": {"signal": "signal_date"}
        }
      }
    }
  ]
}
Message has been deleted

Roy I

unread,
Oct 12, 2016, 9:52:00 AM10/12/16
to vega-js
Found the reason for the glitch: @plot_area:mouseout is triggered when hover over mark rule (vertical line) and other marks (line and text)

Solution: Move mark rect "plot_area" after all other marks and change fillOpacity to 0.0 


On Monday, October 10, 2016 at 8:51:14 PM UTC-4, Calvin Young wrote:

Calvin Young

unread,
Oct 12, 2016, 2:23:03 PM10/12/16
to vega-js
Awesome, thank you — this makes a lot of sense.

I noticed that you set `signal_date` to return `datetime('')` when the mouse isn't in @plot_area. This seems to allow you to omit the additional production rule when setting the x value in your "rule" mark.

1) Is this the best practice for initializing null dates?
2) Why does this not cause Vega to crash? If I initialize `signal_date` with a value of `null`, then I get the following error:

Error: <rect> attribute width: A negative value is not valid. ("-110104")

It seems like both `null` and `Invalid Date` should be equally invalid values for an x-coordinate.

Roy I

unread,
Oct 13, 2016, 4:41:33 PM10/13/16
to vega-js
Not best practice -- just what works in this situation based on trial and error.
The Vega expression "datetime('')" results in the error message (text string) "Invalid Date".
For Vega 2.6.3, it appears that any arbitrary string will do -- but not empty string, number or null.

example:
...
"init": "Hello",
...
{ "type": "@plot_area:mouseout", expr: "'Hello'" }
...
"test": "signal_date == 'Hello'", 


Vega v2.6.3 uses d3.js v3 library including scale.
My guess is that it has to do with the behavior of d3.time.scale:

Also, upcoming Vega v3 has many changes and uses the new d3.js v4 libraries.
What works in Vega v2.6.3 may not work the same way in v3. 





On Monday, October 10, 2016 at 8:51:14 PM UTC-4, Calvin Young wrote:

Calvin Young

unread,
Oct 14, 2016, 3:05:53 PM10/14/16
to vega-js
I see, this makes sense. Thank you!
Reply all
Reply to author
Forward
0 new messages