Skip to content

Commit

Permalink
Closes #62; Removed Dependance on jQuery
Browse files Browse the repository at this point in the history
Completely removed jQuery as a dependancy of Epoch by creating a mini querying system for the library. Basically I hunted down all the ways that jQuery was being used and ensured that we mirrored the functionality using regular JavaScript.

The new querying system, named `Epoch.Query` is where most of the heavy lifting takes place. In places where mirroring functionality seemed out of scope, or could be easily achieved with a simple call or two, I opted not to mirror functionality. One example is that of generating elements directly from html strings.

This change touches quite a few places in the library and fundamentally changes the way we handle DOM querying and manipulation. As such, it would be prudent to get a few eyes on these changes before merging it into master.
  • Loading branch information
rsandor committed Jun 28, 2014
1 parent 512ce64 commit 6059249
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 52 deletions.
32 changes: 17 additions & 15 deletions coffee/adapters/jQuery.coffee
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
do ->
return unless window.jQuery

# Data key to use for storing a reference to the chart instance on an element.
DATA_NAME = 'epoch-chart'
# Data key to use for storing a reference to the chart instance on an element.
DATA_NAME = 'epoch-chart'

# Adds an Epoch chart of the given type to the referenced element.
# @param [Object] options Options for the chart.
# @option options [String] type The type of chart to append to the referenced element.
# @return [Object] The chart instance that was associated with the containing element.
jQuery.fn.epoch = (options) ->
options.el = @
unless (chart = @.data(DATA_NAME))?
klass = Epoch._typeMap[options.type]
unless klass?
Epoch.exception "Unknown chart type '#{options.type}'"
@.data DATA_NAME, (chart = new klass options)
chart.draw()
return chart
# Adds an Epoch chart of the given type to the referenced element.
# @param [Object] options Options for the chart.
# @option options [String] type The type of chart to append to the referenced element.
# @return [Object] The chart instance that was associated with the containing element.
jQuery.fn.epoch = (options) ->
options.el = @.get(0)
unless (chart = @.data(DATA_NAME))?
klass = Epoch._typeMap[options.type]
unless klass?
Epoch.exception "Unknown chart type '#{options.type}'"
@.data DATA_NAME, (chart = new klass options)
chart.draw()
return chart
201 changes: 170 additions & 31 deletions coffee/epoch.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,39 @@ window.Epoch.Time ?= {}
window.Epoch.Util ?= {}
window.Epoch.Formats ?= {}

typeFunction = (objectName) -> (v) ->
Object::toString.call(v) == "[object #{objectName}]"

# @return [Boolean] <code>true</code> if the given value is an array, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isArray = (v) -> jQuery.type(v) == 'array'
Epoch.isArray = Array.isArray or typeFunction('Array')

# @return [Boolean] <code>true</code> if the given value is an object, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isObject = (v) -> jQuery.type(v) == 'object'
Epoch.isObject = typeFunction('Object')

# @return [Boolean] <code>true</code> if the given value is a string, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isString = (v) -> jQuery.type(v) == 'string'
Epoch.isString = typeFunction('String')

# @return [Boolean] <code>true</code> if the given value is a function, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isFunction = (v) -> jQuery.type(v) == 'function'
Epoch.isFunction = typeFunction('Function')

# @return [Boolean] <code>true</code> if the given value is a number, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isNumber = typeFunction('Number')

# Attempts to determine if a given value represents a DOM element. The result is always correct if the
# browser implements DOM Level 2, but one can fool it on certain versions of IE. Adapted from:
# <a href="http://goo.gl/yaD9hV">Stack Overflow #384286</a>.
# @return [Boolean] <code>true</code> if the given value is a DOM element, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isElement = (v) ->
if HTMLElement?
v instanceof HTMLElement
else
v? and Epoch.isObject(v) and v.nodeType == 1 and Epoch.isString(v.nodeName)

# Sends a warning to the developer console with the given message.
# @param [String] msg Message for the warning.
Expand All @@ -30,6 +48,7 @@ Epoch.warn = (msg) ->
Epoch.exception = (msg) ->
throw "Epoch Error: #{msg}"

# Generates shallow copy of an object.
# @return A shallow copy of the given object.
# @param [Object] original Object for which to make the shallow copy.
Epoch.Util.copy = (original) ->
Expand All @@ -44,15 +63,20 @@ Epoch.Util.copy = (original) ->
Epoch.Util.defaults = (options, defaults) ->
result = Epoch.Util.copy(options)
for k, v of defaults
if options[k]? and defaults[k]?
if !Epoch.isArray(options[k]) and Epoch.isObject(options[k]) and Epoch.isObject(defaults[k])
result[k] = Epoch.Util.defaults(options[k], defaults[k])
opt = options[k]
def = defaults[k]
bothAreObjects = Epoch.isObject(opt) and Epoch.isObject(def)

if opt? and def?
if bothAreObjects and not Epoch.isArray(opt)
result[k] = Epoch.Util.defaults(opt, def)
else
result[k] = options[k]
else if options[k]?
result[k] = options[k]
result[k] = opt
else if opt?
result[k] = opt
else
result[k] = defaults[k]
result[k] = def

return result

# Formats numbers with standard postfixes (e.g. K, M, G)
Expand Down Expand Up @@ -108,6 +132,14 @@ Epoch.Util.domain = (layers, key='x') ->
set[entry[key]] = true
return domain

# Strips whitespace from the beginning and end of a string.
# @param [String] string String to trim.
# @return [String] The string without leading or trailing whitespace.
# Returns null if the given parameter was not a string.
Epoch.Util.trim = (string) ->
return null unless Epoch.isString(string)
string.replace(/^\s+/g, '').replace(/\s+$/g, '')

# Converts a CSS color string into an RGBA string with the given opacity
# @param [String] color Color string to convert into an rgba
# @param [Number] opacity Opacity to use for the resulting color.
Expand Down Expand Up @@ -135,6 +167,105 @@ d3Seconds = d3.time.format('%I:%M:%S %p')
# Tick formatter for bytes
Epoch.Formats.bytes = (d) -> Epoch.Util.formatBytes(d)

# Simple DOM querying, lookup, and mutation module. Created to remove the dependency on jQuery.
Epoch.Query = do ->
queryAll = (selector) ->
document.querySelectorAll(selector)

getComputedStyle = (element, pseudoElement) ->
if Epoch.isFunction(window.getComputedStyle)
window.getComputedStyle(element, pseudoElement)
else if element.currentStyle?
element.currentStyle

class Context
getDimension = (name, index=0) ->
return null unless @_hasIndex(index)
element = @get(index)
style = getComputedStyle(element, null)[name]
parseInt( style.substr(0, style.length-2) )

constructor: (@options={}) ->
if @options.element?
@elements = [@options.element]
else if @options.selector?
@elements = queryAll(@options.selector)

_hasElements: ->
@elements.length > 0

_hasIndex: (index) ->
@_hasElements() and index > -1 and index < @elements.length

get: (index=0) ->
return null unless @_hasIndex(index)
@elements[index]

width: (index) ->
getDimension.call @, 'width', index

height: (index) ->
getDimension.call @, 'height', index

css: (name, value, index=0) ->
return null unless @_hasIndex(index)

if Epoch.isString(name) and not value?
return getComputedStyle( @get(index), null )[name]
else if Epoch.isString(name)
el.style[name] = value for el in @elements
else if Epoch.isObject(name)
for key, value of name
el.style[key] = value for el in @elements

return @

append: (element, index=0) ->
return null unless @_hasIndex(index)
if element instanceof Context
@elements[index].appendChild(element.get(0))
else if Epoch.isElement(element)
@elements[index].appendChild(element)
return @

attr: (name, value, index=0) ->
return null unless @_hasIndex(index)

if Epoch.isString(name) and value?
el[name] = value for el in @elements
else if Epoch.isString(name)
return @get(index)[name]
else if Epoch.isObject(name)
for key, value of name
el[name] = value for el in @elements
return @

data: (key, value, index=0) ->
return null unless @_hasIndex(index)
if value?
el["data-#{key}"] = value for el in @elements
return @
@elements[index]["data-#{key}"]

remove: ->
el.parentNode.removeChild(el) for el in @elements

html: (string) ->
return null unless @_hasElements()
el.innerHTML = string for el in @elements

Query = (selector) ->
return null unless selector?
if Epoch.isString(selector)
new Context {selector: selector}
else if Epoch.isElement(selector)
new Context {element: selector}
else if selector instanceof Context
selector

return Query



# Basic eventing base class for all Epoch classes.
class Epoch.Events
Expand Down Expand Up @@ -193,23 +324,24 @@ class Epoch.Chart.Base extends Epoch.Events

@setData(@options.data or [])

@el = jQuery(@options.el) if @options.el?
@el = Epoch.Query(@options.el) if @options.el?

@width = @options.width
@height = @options.height

if @el?
@width = jQuery(@el).width() unless @width?
@height = jQuery(@el).height() unless @height?
@width = @el.width() unless @width?
@height = @el.height() unless @height?
else
@width = defaults.dimensions.width unless @width?
@height = defaults.dimensions.height unless @height?
@width = defaults.width unless @width?
@height = defaults.height unless @height?

# Determines if the chart is currently visible in a document.
# @return [Boolean] True if the chart is visible, false otherwise.
isVisible: ->
return false unless @el?
@el.is(':visible')
return true
#return false unless @el?
#@el.is('*:visible')

# Set the initial data for the chart.
# @param data Data to initially set for the given chart. The data format can vary
Expand Down Expand Up @@ -278,7 +410,7 @@ class Epoch.Chart.Canvas extends Epoch.Chart.Base
# @option options [Array] data Layered data used to render the chart.
constructor: (@options={}) ->
super(@options)
@canvas = jQuery("<canvas></canvas>")
@canvas = Epoch.Query( document.createElement('CANVAS') )

if @options.pixelRatio?
@pixelRatio = @options.pixelRatio
Expand Down Expand Up @@ -315,6 +447,9 @@ class Epoch.Chart.Canvas extends Epoch.Chart.Base
# This allows canvas based visualizations to use the same styles as their
# SVG counterparts.
class QueryCSS
# Reference container id
REFERENCE_CONTAINER_ID = '_canvas_css_reference'

# Handles automatic container id generation
containerCount = 0
nextContainerId = -> "epoch-container-#{containerCount++}"
Expand All @@ -335,17 +470,19 @@ class QueryCSS
# Gets the reference element container.
@getContainer: ->
return QueryCSS.container if QueryCSS.container?
jQuery('body').append('<div id="_canvas_css_reference"></div>')
QueryCSS.container = jQuery('#_canvas_css_reference', 'body')
container = document.createElement('DIV')
container.id = REFERENCE_CONTAINER_ID
document.body.appendChild(container)
QueryCSS.container = Epoch.Query(container)

# @return [String] A unique identifier for the given container and selector.
# @param [String] selector Selector from which to derive the styles
# @param container The containing element for a chart.
@hash: (selector, container) ->
containerId = jQuery(container).data('epoch-container-id')
containerId = Epoch.Query(container).data('epoch-container-id')
unless containerId?
containerId = nextContainerId()
jQuery(container).data('epoch-container-id', containerId)
Epoch.Query(container).data('epoch-container-id', containerId)
return "#{containerId}__#{selector}"

# @return The computed styles for the given selector in the given container element.
Expand All @@ -359,23 +496,25 @@ class QueryCSS

# 1) Build a full reference tree (parents, container, and selector elements)
parents = []
for element in jQuery(container).parents()
break if element.tagName.toLowerCase() == 'body'
parents.unshift(element)
parents.push jQuery(container).get(0)
parentNode = container.get(0).parentNode

while parentNode? and parentNode.nodeName.toLowerCase() != 'body'
parents.unshift parentNode
parentNode = parentNode.parentNode
parents.push container.get(0)

selectorList = []
for element in parents
sel = element.tagName.toLowerCase()
sel = element.nodeName.toLowerCase()
if element.id? and element.id.length > 0
sel += '#' + element.id
if element.className? and element.className.length > 0
sel += '.' + jQuery.trim(element.className).replace(/\s+/g, '.')
sel += '.' + Epoch.Util.trim(element.className).replace(/\s+/g, '.')
selectorList.push sel

selectorList.push('svg')

for subSelector in jQuery.trim(selector).split(/\s+/)
for subSelector in Epoch.Util.trim(selector).split(/\s+/)
selectorList.push(subSelector)

parent = root = put(selectorList.shift())
Expand All @@ -386,7 +525,7 @@ class QueryCSS

# 2) Place the reference tree and fetch styles given the selector
QueryCSS.getContainer().append(root)
ref = jQuery(selector, root)
ref = Epoch.Query('#' + REFERENCE_CONTAINER_ID + ' ' + selector)
styles = {}
for name in QueryCSS.styleList
styles[name] = ref.css(name)
Expand Down
8 changes: 4 additions & 4 deletions coffee/time.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ class Epoch.Time.Plot extends Epoch.Chart.Canvas
.attr('dy', 19)
.text(@options.tickFormats.bottom(tick.time))

tick.bottomEl = jQuery(g[0])
tick.bottomEl = g

if @hasAxis('top')
g = @topAxis.append('g')
Expand All @@ -408,7 +408,7 @@ class Epoch.Time.Plot extends Epoch.Chart.Canvas
.attr('dy', -10)
.text(@options.tickFormats.top(tick.time))

tick.topEl = jQuery(g[0])
tick.topEl = g

if reverse
@_ticks.unshift tick
Expand Down Expand Up @@ -441,8 +441,8 @@ class Epoch.Time.Plot extends Epoch.Chart.Canvas
tick.opacity -= dop

if tick.enter or tick.exit
tick.bottomEl.css('opacity', tick.opacity) if @hasAxis('bottom')
tick.topEl.css('opacity', tick.opacity) if @hasAxis('top')
tick.bottomEl.style('opacity', tick.opacity) if @hasAxis('bottom')
tick.topEl.style('opacity', tick.opacity) if @hasAxis('top')

# Draws the visualization in the plot's canvas.
# @param delta The current x offset to apply to all elements when rendering. This number
Expand Down
2 changes: 1 addition & 1 deletion coffee/time/gauge.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Epoch.Time.Gauge extends Epoch.Chart.Canvas
.attr('height', @height)
.attr('class', 'gauge-labels')

jQuery(@svg[0]).css
Epoch.Query(@svg[0]).css
'position': 'absolute'
'z-index': '1'

Expand Down
Loading

0 comments on commit 6059249

Please sign in to comment.