layout using matterjs engine, with additional machinery for improving the layout

This is still in progress, but want to checkpoint the work to date

--- @client/histogram.coffee
+++ @client/histogram.coffee
@@ -1,4 +1,5 @@
 require './browser_hacks'
+require './histogram_lab'
 # md5 = require './vendor/md5'

 ##
@@ -7,7 +8,7 @@
 # Controls the display of the users arranged on a histogram.
 #
 # The user avatars are arranged imprecisely on the histogram
-# based on the user's opinion, using a physics simulation.
+# based on the user's opinion, using a layout_params simulation.
 #
 # The pros and cons can be filtered to specific opinion regions
 # (individual and collective).
@@ -101,15 +102,15 @@
 #
 # Local state:
 #   simulation_opinion_hash
-#      Hash of all opinion stances. Used to determine if the physics
+#      Hash of all opinion stances. Used to determine if the layout_params
 #      simulation needs to be rerun on a rerender.
 #   mouse_opinion_value
 #      Stores the mapped opinion value of the current mouse position
 #      within the histogram.
 #   avatar_size
-#      The base size of the avatars, as determined by the physics
+#      The base size of the avatars, as determined by the layout_params
 #      simulation. This piece of state would be local, but it needs
-#      to be settable from the physics simulation.
+#      to be settable from the layout_params simulation.

 # Accessibility notes:
 #   - histogram itself should be tabbable. Should summarize results.
@@ -150,12 +151,20 @@

 ENABLE_SERVER_HISTOCACHE = false

+
+window.show_histogram_physics = false
+
 window.Histogram = ReactiveComponent
   displayName : 'Histogram'

   get_hist_state: -> fetch (@props.selection_state or @props.key)

   render: ->
+
+    loc = fetch 'location'
+    if loc.query_params.show_histogram_physics
+      window.show_histogram_physics = true
+
     hist = @get_hist_state()

     dirtied = false
@@ -163,16 +172,19 @@
       reset_selection_state(hist)
       dirtied = true

-    avatar_radius = calculateAvatarRadius(@props.width, @props.height, @props.opinions)
-
-    if @local.avatar_size != avatar_radius * 2
-      @local.avatar_size = avatar_radius * 2
-      save @local
-      dirtied = true

     # extraction from @try_histocache
     proposal = fetch(@props.proposal)
     histocache_key = @histocache_key()
+    if @last_key != histocache_key
+      avatar_radius = calculateAvatarRadius(@props.width, @props.height, @props.opinions, {fill_ratio: @props.layout_params?.fill_ratio or 1})
+
+      if @local.avatar_size != avatar_radius * 2
+        @local.avatar_size = avatar_radius * 2
+        save @local
+        dirtied = true
+
+      @last_key = histocache_key
     if ENABLE_SERVER_HISTOCACHE && proposal.histocache?[histocache_key]
       if histocache_key != @local.histocache?.hash
         @local.histocache =
@@ -330,7 +342,7 @@

       DIV
         style:
-          paddingTop: histo_height - @props.height
+          paddingTop: histo_height - @props.height

         HistoAvatars
           dirtied: dirtied
@@ -557,7 +569,7 @@

     false

-  physicsSimulation: ->
+  PhysicsSimulation: ->
     filter_out = fetch 'filtered'
     proposal = fetch @props.proposal

@@ -577,14 +589,43 @@
       opinions = for opinion, i in filtered_opinions
         {stance: opinion.stance, user: opinion.user}

-      setTimeout =>
+      # use a less cpu intensive layout for mobile browsers
+      layout_params = _.defaults {}, (@props.layout_params or {}),
+        engine: if browser.is_mobile then 'tile' else 'matterjs'
+        initial_layout: if browser.is_mobile then 'packed-tile' else 'tiled-with-wiggle'
+        fill_ratio: 1
+        show_histogram_physics: show_histogram_physics
+        motionSleepThreshold: .005
+        motionSleepThresholdIncrement: .000025
+        enable_boosting: false
+        wake_every_x_ticks: 9999
+        global_swap_every_x_ticks: 150
+        reduce_sleep_threshold_if_little_movement: true
+        sleep_reduction_exponent: .5
+        cleanup_layout_every_x_ticks: 50
+        x_force_mult: .008
+        gravity_scale: .000012
+        restack_top: false
+        final_cleanup_stability: .5
+        change_cleanup_stability: -0.2
+        cleanup_stability: .9
+        cleanup_when_percent_sleeping: .4
+        end_sleep_percent: .75
+        filter_to_inflections_and_flats: true
+        cleanup_overlap: 1.8
+        cascade_instability: true
+
+
+      setTimeout =>
         if @isMounted()
-          layoutAvatars
+          layoutAvatars
+            running_state: @props.key
             k: histocache_key
             w: @props.width
             h: @props.height
             o: opinions
             r: @local.avatar_size / 2
+            layout_params: layout_params
             abort: =>
               abort = !@isMounted() || @current_request != histocache_key
               abort
@@ -605,19 +646,33 @@
                   save
                     key: "/histogram/proposal/#{fetch(@props.proposal).id}/#{histocache_key}"
                     positions: positions
-      , 1
+      , 0

     @current_request = histocache_key

-
-
-
   componentDidMount: ->
-    @physicsSimulation()
+    @PhysicsSimulation()

   componentDidUpdate: ->
-    @physicsSimulation()
-
+    @PhysicsSimulation()
+
+
+window.styles += """
+  .histo_avatar.avatar {
+    position: absolute;
+    cursor: pointer;
+  }
+  img.histo_avatar.avatar {
+    z-index: 1;
+  }
+  .histo_avatar.avatar.selected {
+    z-index: 9;
+    background-color: #{focus_color()};
+  }
+  .histo_avatar.avatar.not_selected {
+    opacity: .2;
+  }
+"""

 HistoAvatars = ReactiveComponent
   displayName: 'HistoAvatars'
@@ -646,19 +701,14 @@
     regular_avatar_style =
       width: @props.avatar_size
       height: @props.avatar_size
-      position: 'absolute'
-      cursor: if @props.enable_individual_selection then 'pointer' else 'auto'
+      cursor: if !@props.enable_individual_selection then 'auto'

     # 2) The style of a selected avatar
-    selected_avatar_style = _.extend {}, regular_avatar_style,
-      zIndex: 9
-      backgroundColor: focus_color()
-    css.crossbrowserify selected_avatar_style
+    selected_avatar_style = regular_avatar_style
+
     # 3) The style of an unselected avatar when some other avatar(s) is selected
-    unselected_avatar_style = _.extend {}, regular_avatar_style,
-      opacity: .2
-    # if !browser.is_mobile
-    #   unselected_avatar_style = css.grayscale _.extend unselected_avatar_style
+    unselected_avatar_style = regular_avatar_style
+
     # 4) The style of the avatar when the histogram is backgrounded
     #    (e.g. on the crafting page)
     backgrounded_page_avatar_style = _.extend {}, unselected_avatar_style,
@@ -666,7 +716,11 @@

     # Draw the avatars in the histogram. Placement will be determined later
     # by the physics sim
+
+
+    users = {}
     DIV
+      id: @props.histocache_key
       key: @props.histocache_key
       ref: 'histo'
       style:
@@ -679,6 +733,8 @@
       for opinion, idx in @props.opinions
         user = opinion.user

+        users[opinion.user.key or opinion.user] = opinion
+
         backgrounded = filter_out.users?[user]
         if backgrounded && !filter_out.enable_comparison
           continue
@@ -689,6 +745,7 @@
         # creation = new Date(o.created_at).getTime()
         # opacity = .05 + .95 * (creation - sub_creation) / (Date.now() - sub_creation)

+        className = 'histo_avatar'
         if backgrounded || @props.backgrounded
           avatar_style = if fetch('/current_user').user == user
                            _.extend({}, regular_avatar_style, {opacity: .25})
@@ -697,20 +754,16 @@
         else if highlighted_users
           if _.contains(highlighted_users, opinion.user)
             avatar_style = selected_avatar_style
+            className += " selected"
           else
             avatar_style = unselected_avatar_style
+            className += " not_selected"
         else
           avatar_style = regular_avatar_style

         pos = @props.histocache?.positions?[(user.key or user).substring(6)]
-        # Avatar
-        #   key: user
-        #   user: user
-        #   hide_tooltip: @props.backgrounded
-        #   style: _.extend {}, avatar_style,
-        #     left: pos?[0]
-        #     top: pos?[1]
-        #     # opacity: opacity
+        continue if !pos
+

         stance = opinion.stance
         if stance > .01
@@ -732,23 +785,26 @@
           ref: "avatar-#{idx}"
           focusable: @props.navigating_inside && !@props.backgrounded && !backgrounded
           hide_tooltip: @props.backgrounded || backgrounded
-          alt: "<user>: #{alt}"
+          className: className
+          alt: "<user>: #{alt} #{pos[3]?.toFixed(3)}"
+          anonymous: true
           style: _.extend {}, avatar_style,
             left: pos?[0]
             top: pos?[1]
-            # opacity: opacity
-
-
-
-
-######
-# Uses a d3-based physics simulation to calculate a reasonable layout
-# of avatars within a given area.
+            # border: "2px solid #{if pos[2] then '#999' else 'orange'}"
+            # border: "2px solid #{if pos[3] < .7 && pos[3] > .5 then 'red' else if pos[3] <= .5 then 'red' else '#999'}"
+            #border: "1px solid #{hsv2rgb(1 - pos[3] * .8, .5, .5)}" # #{if pos[3] <= .5 then 'red' else '#999'}"
+            backgroundColor: "#{hsv2rgb(pos[3] or .3, 1 - pos[3] or .5, .5)}" # #{if pos[3] <= .5 then 'red' else '#999'}"
+
+
+

 layoutAvatars = (opts) ->
   histo_queue.push opts
   if !histo_running
     histo_run_next_job()
+
+

 histo_queue = []
 histo_running = null
@@ -759,10 +815,1315 @@
   if !histo_running && histo_queue.length > 0
     histo_running = histo_queue.shift()

-    positionAvatars histo_running
-
-
-positionAvatars = (opts) ->
+    if histo_running.layout_params.engine == 'matterjs'
+      positionAvatarsWithMatterJS histo_running
+    else if histo_running.layout_params.engine == 'd3'
+      positionAvatarsWithD3 histo_running
+    else
+      positionAvatarsWithJustLayout histo_running
+
+
+
+initialize_positions = (opts) ->
+  targets = {}
+  opinions = opts.o.slice()
+  width = opts.w or 400
+  height = opts.h or 70
+  layout_params = opts.layout_params
+  r = opts.r
+
+  if layout_params.initial_layout == 'classic_at_top'
+    nodes = classic_layout(opinions, width, height, r, targets, at_top: true)
+  else if layout_params.initial_layout == 'classic_at_bottom'
+    nodes = classic_layout(opinions, width, height, r, targets, at_top: false)
+  else if layout_params.initial_layout == 'tiled' || !layout_params.initial_layout
+    nodes = tiled_layout(opinions, width, height, r, targets)
+  else if layout_params.initial_layout == 'tiled-with-wiggle'
+    nodes = tiled_layout(opinions, width, height, r, targets, wiggle: 1)
+  else if layout_params.initial_layout == 'tiled-with-wiggle-2'
+    nodes = tiled_layout(opinions, width, height, r, targets, wiggle: .5)
+
+  else if layout_params.initial_layout == 'packed-tile'
+    nodes = packed_tiled_layout(opinions, width, height, r, targets, wiggle: false)
+
+
+  {nodes, targets, opinions, width, height, r}
+
+
+classic_layout = (opinions, width, height, r, targets, {at_top}) ->
+  opinions.map (o, i) ->
+    x_target = (o.stance + 1) / 2 * width
+
+    if targets[x_target]
+      if x_target > .98
+        x_target -= .1 * Math.random()
+      else if x_target < .02
+        x_target += .1 * Math.random()
+
+    targets[x_target] = 1
+
+    x = x_target
+    if at_top
+      y = r
+    else
+      y = height - r
+
+    return {
+      index: i
+      radius: r
+      x: x
+      y: y
+      x_target: x_target
+    }
+
+tiled_layout = (opinions, width, height, r, targets, params) ->
+  MAX_DISTURBANCE = 1 * width # how far away from its target is an opinion allowed to start?
+  laid_out = []
+  return laid_out if opinions.length == 0
+
+  wiggle = params?.wiggle
+
+  find_split = (start, end, iterations) ->
+    halfway = Math.round((end - start) / 2) + start
+    if iterations > 4
+      return halfway
+
+    if opinions[start].stance < 0 && opinions[halfway].stance < 0
+      return find_split(halfway, end, iterations + 1)
+    else
+      return find_split(start, halfway, iterations + 1)
+
+  split_idx = find_split(0, opinions.length - 1, 0)
+  num_cols = Math.round(width / (2 * r))
+  num_rows = Math.round(height / (2 * r))
+  grid = []
+  current_row = []
+
+  col = 0
+  while col < num_cols
+    grid[col] = []
+    current_row[col] = 0
+    row = 0
+    while row < num_rows
+      grid[col][row] = 0
+      row +=1
+    col += 1
+
+  idx = 0
+  while idx < split_idx
+    x_target = (opinions[idx].stance + 1) / 2 * width
+
+    target_col = Math.round( (opinions[idx].stance + 1) / 2 * num_cols   )
+
+    if target_col >= num_cols
+      target_col = num_cols - 1
+    else if target_col < 0
+      target_col = 0
+
+    col = target_col
+
+    # find a good position for this node
+    assigned = false
+    cnt_in_target = grid[target_col][0]
+    while (col - target_col) * 2 * r < MAX_DISTURBANCE && col < num_cols
+      if grid[col][current_row[col]] < cnt_in_target
+        row = current_row[col]
+        assigned = true
+        break
+      col += 1
+
+    if !assigned
+      col = target_col
+      row = 0
+
+    grid[col][row] += 1
+    if row == num_rows - 1
+      current_row[col] = 0
+    else
+      current_row[col] += 1
+
+    x = col * 2 * r + r
+    y = height - (row * 2 * r) - r
+
+    laid_out.push {
+      index: idx
+      radius: r
+      x: x
+      y: y
+      x_target: x_target
+    }
+    idx += 1
+
+  idx = opinions.length - 1
+  while idx >= split_idx
+    x_target = (opinions[idx].stance + 1) / 2 * width
+
+    target_col = Math.round( (opinions[idx].stance + 1) / 2 * num_cols   ) - 1
+
+    if target_col >= num_cols
+      target_col = num_cols - 1
+    else if target_col < 0
+      target_col = 0
+
+    col = target_col
+
+    # find a good position for this node
+    assigned = false
+    cnt_in_target = grid[target_col][0]
+    while (target_col - col) * 2 * r < MAX_DISTURBANCE && col >= 0
+      if grid[col][current_row[col]] < cnt_in_target
+        row = current_row[col]
+        assigned = true
+        break
+      col -= 1
+
+    if !assigned
+      col = target_col
+      row = 0
+
+    grid[col][row] += 1
+    if row == num_rows - 1
+      current_row[col] = 0
+    else
+      current_row[col] += 1
+
+    x = col * 2 * r + r
+    y = height - (row * 2 * r) - r
+
+    laid_out.push {
+      index: idx
+      radius: r
+      x: x
+      y: y
+      x_target: x_target
+    }
+    idx -= 1
+
+  if wiggle
+    for n in laid_out
+      n.x += (Math.random() - .5) * r * wiggle
+      # n.y += (Math.random() - .5) * r
+      if n.x + r > width
+        n.x = width - r
+      else if n.x < r
+        n.x = r
+      if n.y + r > height
+        n.y = height - r
+      else if n.y < r
+        n.y = r
+
+
+  laid_out.sort (a,b) -> a.index - b.index
+  laid_out
+
+# packed_tiled_layout
+# Calculated initial position of each avatar by laying them out in a grid as close to their
+# intended position as possible, starting from the poles and working to the middle.
+# This is "packed" because it leverages the avatars being circles to make each column
+# less than 2*r wide. Each cicle only needs to have their centroid 2*r away from each other one,
+# so if we alternate the initial y position of each column by r, we can actually use spacing
+# r + (sqrt(2) * r) / 2 rather than 2*r. This may sound like a small improvement, but the asthetics
+# are quite profound.
+# Note: I changed to packing the row_height, rather than col_width, but same principles apply
+packed_tiled_layout = (opinions, width, height, r, targets, params) ->
+  MAX_DISTURBANCE = 1 * width # how far away from its target is an opinion allowed to start?
+  laid_out = []
+  return laid_out if opinions.length == 0
+
+  wiggle = params?.wiggle
+
+  find_split = (start, end, iterations) ->
+    halfway = Math.round((end - start) / 2) + start
+    if iterations > 4
+      return halfway
+
+    if opinions[start].stance < 0 && opinions[halfway].stance < 0
+      return find_split(halfway, end, iterations + 1)
+    else
+      return find_split(start, halfway, iterations + 1)
+
+  split_idx = find_split(0, opinions.length - 1, 0)
+
+  col_width = 2 * r
+  row_height = r + (r * Math.sqrt(2)) / 2
+  num_cols = Math.round(width / col_width)
+  num_rows = Math.round(height / row_height)  # -.5 * r is to accommodate the avg offset for packing (0 vs r)
+  grid = []
+  current_row = []
+
+  col = 0
+  while col < num_cols
+    grid[col] = []
+    current_row[col] = 0
+    row = 0
+    while row < num_rows
+      grid[col][row] = 0
+      row +=1
+    col += 1
+
+
+  for negative_pole in [true, false]
+    idx = if negative_pole then 0 else opinions.length - 1
+
+    while (negative_pole && idx < split_idx) || (!negative_pole && idx >= split_idx)
+      adjusted_stance = (opinions[idx].stance + 1) / 2
+      x_target = adjusted_stance * width
+      target_col = Math.round( adjusted_stance * num_cols  )
+
+      if !negative_pole
+        target_col -= 1
+
+      if target_col >= num_cols
+        target_col = num_cols - 1
+      else if target_col < 0
+        target_col = 0
+
+      col = target_col
+
+      # find a good position for this node
+      assigned = false
+      cnt_in_target = grid[target_col][0]
+
+      if negative_pole
+        while (col - target_col) * 2 * r < MAX_DISTURBANCE && col < num_cols
+          if grid[col][current_row[col]] < cnt_in_target
+            row = current_row[col]
+            assigned = true
+            break
+          col += 1
+      else
+        while (target_col - col) * 2 * r < MAX_DISTURBANCE && col >= 0
+          if grid[col][current_row[col]] < cnt_in_target
+            row = current_row[col]
+            assigned = true
+            break
+          col -= 1
+
+      if !assigned
+        col = target_col
+        row = 0
+
+      grid[col][row] += 1
+      if row == num_rows - 1
+        current_row[col] = 0
+      else
+        current_row[col] += 1
+
+      x = col * col_width + r
+      y = height - (row * row_height) - r
+      if row % 2 == 1
+        # offset for packing
+        if x_target < 0
+          x -= r
+        else
+          x += r
+
+      laid_out.push {
+        index: idx
+        radius: r
+        x: x
+        y: y
+        x_target: x_target
+      }
+
+      if negative_pole
+        idx += 1
+      else
+        idx -= 1
+
+
+
+
+  if wiggle
+    for n in laid_out
+      n.x += (Math.random() - .5) * r
+      # n.y += (Math.random() - .5) * r
+      if n.x + r > width
+        n.x = width - r
+      else if n.x < r
+        n.x = r
+      if n.y + r > height
+        n.y = height - r
+      else if n.y < r
+        n.y = r
+
+
+  laid_out.sort (a,b) -> a.index - b.index
+  laid_out
+
+
+#####
+# Physics engine for reasonable layout of avatars within area
+#
+
+positionAvatarsWithJustLayout = (opts) ->
+  {nodes, targets, opinions, width, height, r} = initialize_positions opts
+
+  positions = {}
+  for n in nodes
+    r = n.radius
+    i = n.index
+    positions[parseInt(opinions[i].user.substring(6))] = \
+      [Math.round((n.x - r) * 10) / 10, Math.round((n.y - r) * 10) / 10]
+
+  opts.done?(positions)
+  histo_run_next_job(opts)
+
+
+
+positionAvatarsWithMatterJS = (opts) ->
+  Events = Matter.Events
+  Engine = Matter.Engine
+  init = initialize_positions opts
+  width = init.width
+  height = init.height
+  opinions = init.opinions
+
+  layout_params = opts.layout_params
+
+  # create engine
+  engine = Engine.create()
+  engine.enableSleeping = true
+
+  #############
+  # add walls
+  wall = 1000
+  wall_params = { isStatic: true, render: strokeStyle: 'transparent' }
+  Matter.Composite.add engine.world, [
+    # top
+    Matter.Bodies.rectangle(  width / 2, -wall / 2, 2 * width, wall, wall_params)
+    # bottom
+    Matter.Bodies.rectangle(  width / 2, height + wall / 2, 2 * width + wall / 2, wall, wall_params)
+    # right
+    Matter.Bodies.rectangle(  width + wall / 2, height / 2, wall, 2 * height + wall, wall_params)
+    # left
+    Matter.Bodies.rectangle(  -wall / 2, height / 2, wall, 2 * height + wall, wall_params)
+  ]
+  #############
+
+  ############
+  # Initialize avatar positions
+  bodies = []
+  params =
+    density: layout_params.density or .1 # significantly heavier than default so they don't overlap as much
+    sleepThreshold: layout_params.sleepThreshold or 60 # This is the default
+    restitution: layout_params.restitution or 0 # Don't bounce off of each other
+    friction: layout_params.friction or .001
+    frictionStatic: layout_params.frictionStatic or .3
+    frictionAir: layout_params.frictionAir or 0
+
+  for n in init.nodes
+    bd = Matter.Bodies.circle n.x, n.y, init.r, params
+    #b = Matter.Bodies.polygon(n.x, n.y, 4, r, params)
+    _.extend bd, n
+    bodies.push bd
+
+    bd.before_sleep =
+      x: bd.position.x
+      y: bd.position.y
+
+    bd.original_sleep_threshold = bd.sleepThreshold
+
+  # sort so we can optimize by knowing that bodies are ordered by x_target
+  bodies.sort (a,b) -> a.x_target - b.x_target
+
+  Matter.Composite.add engine.world, bodies
+  ################
+
+
+
+
+  # Swap the positions and velocities of two avatars if a swap would be worth it
+  attempt_swap = (a,b) ->
+    current_distance = Math.abs(a.position.x - a.x_target) + Math.abs(b.position.x - b.x_target)
+    swapped_distance = Math.abs(b.position.x - a.x_target) +  Math.abs(a.position.x - b.x_target)
+
+    if current_distance - swapped_distance < .01 && Math.abs(a.position.x - b.position.x) < 1
+      return false
+
+    if a.isSleeping
+      Matter.Sleeping.set(a, false)
+    if b.isSleeping
+      Matter.Sleeping.set(b, false)
+
+    # swap positions
+    tmpx = a.position.x
+    tmpy = a.position.y
+
+    Matter.Body.setPosition a, b.position
+    Matter.Body.setPosition b,
+      x: tmpx
+      y: tmpy
+
+    # swap velocity y and zero velocity x
+    tmpy = a.velocity.y
+
+    Matter.Body.setVelocity a,
+      x: 0
+      y: b.velocity.y
+    Matter.Body.setVelocity b,
+      x: 0
+      y: tmpy
+
+    a.sleepCounter = 0
+    b.sleepCounter = 0
+
+    a.moved_insignificantly_after_sleep = 0
+    b.moved_insignificantly_after_sleep = 0
+
+    true
+
+
+  Events.on engine, "collisionStart", (ev) ->
+
+    for pair in ev.pairs
+      a = pair.bodyA
+      b = pair.bodyB
+
+      continue if a.isStatic || b.isStatic
+
+      if (a.position.x < b.position.x && a.x_target > b.x_target) || \
+         (a.position.x > b.position.x && a.x_target < b.x_target)
+
+        attempt_swap(a, b)
+
+
+
+  motionSleepThreshold = layout_params.motionSleepThreshold or .01
+  Matter.Sleeping._motionSleepThreshold = motionSleepThreshold
+                                              # The ease at which bodies fall asleep based on their motion.
+                                              # Much lower than default (.08) b/c otherwise the avatars are
+                                              # too soporific and suspended in air
+  motionSleepThresholdIncrement = layout_params.motionSleepThresholdIncrement or .00001
+                                              # Each tick, we make it slightly easier for bodies to fall asleep. This is like
+                                              # faux simulated annealing to bring the layout to an end faster.
+
+  wake_every_x_ticks = layout_params.wake_every_x_ticks
+  global_swap_every_x_ticks = layout_params.global_swap_every_x_ticks
+  total_ticks = 0
+  total_sleeping = 0
+
+  runner = Matter.Runner.create()
+  try_swap = 0
+  did_swap = 0
+  no_swap = 0
+
+
+  histo_layout_explorer_options = fetch('histo_layout_explorer_options')
+
+
+
+  sweep_all_for_position_swaps = ->
+    for body, i in bodies
+      for body2,j in bodies when i < j
+        # bodies is ordered by x_target, so we know body.x_target <= body2.x_target
+        if body.position.x > body2.position.x && body.x_target != body2.x_target
+          attempt_swap(body, body2)
+
+  write_positions = ->
+    positions = {}
+    for body in bodies
+      o = body.position
+      r = body.radius
+      i = body.index
+      positions[parseInt(opinions[i].user.substring(6))] = \
+        [Math.round((o.x - r) * 10) / 10, Math.round((o.y - r) * 10) / 10, body.isSleeping, body.stability]
+
+    opts.done?(positions)
+
+
+  _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame \
+                                || window.mozRequestAnimationFrame || window.msRequestAnimationFrame \
+                                || (callback) ->
+                                      setTimeout ->
+                                        callback Matter.Common.now()
+                                      , 1000 / 60
+
+  #####################
+  # How stable is this body?
+  # We'll consider the angles of any bodies or edges this body is touching, if any.
+  # The greatest stability is when the maximum angle between touch points in the lower
+  # half circle is just under PI / 2. The worst -- free fall -- is when there are no touch points
+  # in the lower half circle, which is simply PI. If the body is resting on the surface,
+  # the max angle is PI / 2. Thus stability = 1 - (max_arc - best ) / best, which
+  # is 0 for free fall and 1 for resting comfortably.
+  calc_stability = (body, blacklist = {}) ->
+    x = Math.round body.position.x
+    y = Math.round body.position.y
+    r = body.radius
+
+    # resting on the ground
+    return 1 if y + r + 1 >= height
+
+    if neighbors_last_set != total_ticks
+      find_neighbors()
+
+    stabilizing_neighbors = (angle for [other_idx, angle] in body.neighbors when angle < Math.PI && angle > 0 && !blacklist[other_idx])
+
+    # body in freefall
+    return 0 if stabilizing_neighbors.length == 0
+
+    # find the neighbor most centrally below the body
+    most_stable = 2 * Math.PI
+    for angle in stabilizing_neighbors
+      if Math.abs(Math.PI / 2 - angle) < most_stable
+        most_stable = angle
+
+    stabilizing_neighbors.sort()
+
+    neighbors = body.neighbors
+    if x <= r
+      neighbors = neighbors.slice()
+      neighbors.unshift ['left edge', Math.PI]
+    else if x >= width - r
+      neighbors = neighbors.slice()
+      neighbors.unshift ['right edge', 0]
+
+    # search for a counterbalancing body. A counterbalancing body will be within π radians on the lower half circle formed
+    # from the most stable angle
+    for n in neighbors when !blacklist[n[0]]
+      n_angle = n[1]
+      angle_between = n_angle - most_stable
+      if (most_stable < Math.PI / 2 && angle_between > 0 && angle_between < Math.PI) || \
+         (most_stable > Math.PI / 2 && angle_between < 0 && angle_between > -Math.PI)
+        return 1 # has counterbalancing stabilizing body
+
+
+    # The below will be 1 if there's a stabilizing body directly under it,
+    # and 0 if there's one almost directly to its side
+    deviation_from_stable = Math.abs(most_stable - Math.PI / 2)
+    1 - deviation_from_stable / (Math.PI / 2)
+
+
+  evaluate_body_position = (body, occupancy_map) ->
+
+    occupancy_map ?= build_occupancy_map()
+
+    ####################
+    # How truthful is the location based on the user's intended position?
+    dist = Math.abs(body.position.x - body.x_target)
+    dist /= width
+
+    truth = Math.pow 1 - dist, 2
+    body.truth = truth
+
+    ####################
+    # How visible? Is it covered up at all?
+    r = body.radius
+    mask = avatar_mask[r]
+    x = Math.round body.position.x
+    y = Math.round body.position.y
+
+    left = x - r
+    top = y - r
+    overlapped_pixels = 0
+    total_pixels = 0
+    for row in [0..2 * r]
+      for col in [0..2 * r]
+        if mask[row * 2 * r + col] > 0
+          pixel = occupancy_map[(top + row) * width + (left + col)]
+          total_pixels += 1
+          if pixel > 1
+            overlapped_pixels += 1
+
+    percent_visible = 1 - overlapped_pixels / total_pixels
+    body.visibility = percent_visible
+
+
+    body.stability = calc_stability body
+
+
+
+    #####################
+    # How much space below?
+    row = y + r + 2 # +2 instead of +1 to give a little grace
+    col = x         # considering the imprecise rounding when
+                    # translating into pixel space
+    space_below = 0
+    while true
+      break if row >= height
+      pixel = occupancy_map[row * width + col]
+
+      break if pixel > 0
+      space_below += 1
+      row += 1
+
+    # We'll levy a much greater penalty for space if this body could simply
+    # have been moved straight down
+    if space_below > 1
+      could_free_fall = true
+      row = y + r + 2 # +2 instead of +1 to give a little grace
+
+      for ccol in [col - r .. col + r]
+        continue if ccol < 0 || ccol >= width
+        could_free_fall &&= occupancy_map[row * width + ccol] == 0
+        break if !could_free_fall
+
+      if could_free_fall
+        space_below = Math.pow space_below, 3
+
+    body.space_below = space_below
+
+  evaluate_layout = ->
+    occupancy_map = build_occupancy_map()
+
+
+    # judge truth by the *least* well positioned avatar
+    truth = 1
+
+    # judge visibility by the *most* hidden avatar
+    visibility = 1
+
+    # Judge how stability the layout is based on the sum of the space below the *middle* of each body
+    stability = 0
+
+    for body in bodies
+      evaluate_body_position body, occupancy_map
+
+      if body.truth < truth
+        truth = body.truth
+
+      if body.visibility < visibility
+        visibility = body.visibility
+
+      if body.stability?
+        stability += Math.pow body.stability, 2 # body.space_below
+
+    stability /= bodies.length
+    _.extend running_state, {truth, visibility, stability}
+
+    global_running_state[layout_params.param_hash].truth += truth
+    global_running_state[layout_params.param_hash].visibility += visibility
+    global_running_state[layout_params.param_hash].stability += stability
+    global_running_state[layout_params.param_hash].cnt += 1
+
+
+  # Instead of laboriously computing the stability of all possible positions for unstable
+  # bodies to be moved to, we exploit the shape of the lower most openings to heuristically
+  # eliminate unstable positions. Because the avatars are circles, unstable positions are
+  # those where the y position of an opening is rising or falling (putting something on top
+  # at those places would be slippery). So we filter them out by identifying positive (going
+  # from decreasing to increasing) and negative (vice versa) inflection points. These roughly
+  # correspond to a stable position resting on top of a body, and nestled in between two
+  # bodies. Flat areas are also stable.
+  Math.dist = (a,b) ->
+    Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y))
+
+  filter_openings = (openings, target_radius, filter_to_inflections_and_flats) ->
+
+    radius = target_radius or init.radius
+
+    # Find the first opening from bottom up in each col. These are candidate
+    # stable positions to move an unstable body.
+    flattened_openings = []
+    for col in [0..width]
+      opening_this_col = 0
+
+      for row in [height - 1..0 - 1] by -1
+        if row <= height - radius && col >= radius && col <= width - radius && openings[ row * width + col ] == 0
+          opening_this_col = row
+          break
+
+      flattened_openings.push opening_this_col
+
+    if !filter_to_inflections_and_flats
+      return flattened_openings
+
+    candidates = []
+    r_long = Math.round(radius * layout_params.cleanup_overlap) - 1
+    dist = Math.round(radius/2) - 1
+    for i in [0..width]
+      y = flattened_openings[i]
+      y_prev = flattened_openings[i-dist]
+      y_next = flattened_openings[i+dist]
+      hypo_pos =
+        y: y + r_long
+        x: i
+      if (!y_next? || ((hypo_pos.y < y) == ( hypo_pos.y < y_next) && Math.dist(hypo_pos, {x: i+dist, y: y_next}) >= r_long)) && \
+         (!y_prev? || ((hypo_pos.y < y) == ( hypo_pos.y < y_prev) && Math.dist(hypo_pos, {x: i-dist, y: y_prev}) >= r_long))
+
+        candidates.push y
+      else
+        candidates.push 0
+
+    return candidates
+
+
+  _cascade_instability = (body, instabilities, cleanup_stability) ->
+    instabilities[body.index] = 1
+
+    if layout_params.cascade_instability
+      for n in body.neighbors
+        nb = bodies[n[0]]
+        continue if instabilities[nb.index]
+        if calc_stability(nb, instabilities) < cleanup_stability
+          _cascade_instability nb, instabilities, cleanup_stability
+
+
+  get_unstable_bodies = (cleanup_stability) ->
+    instabilities = {}
+    for body in bodies
+      if calc_stability(body) < cleanup_stability
+        _cascade_instability body, instabilities, cleanup_stability
+
+    unstable = (bodies[k] for k,v of instabilities when v == 1)
+    positive = (b for b in unstable when b.x_target <= width)
+    negative = (b for b in unstable when b.x_target > width)
+
+    positive.sort (a,b) -> b.x_target - a.x_target
+    negative.sort (a,b) -> a.x_target - b.x_target
+    negative.concat positive
+
+  get_highest_bodies = ->
+    highest = []
+    for body in bodies
+      continue if body.position.y >= height - body.radius
+      has_body_on_top = false
+      for n in body.neighbors
+        if n[1] > Math.PI * 1.1
+          has_body_on_top = true
+          break
+      if !has_body_on_top
+        highest.push body
+
+    highest
+
+
+  cleanup_overlap = layout_params.cleanup_overlap or 2
+  cleanup_layout = (cleanup_stability) ->
+    cleanup_stability = cleanup_stability or layout_params.cleanup_stability or .7
+
+    self_available = no_self_available = self_should_be_available = below_self = above_self = 0
+
+    if layout_params.verbose
+      running_state.cleanup ?= []
+    current_cleanup = []
+
+    iterations = 0
+    exhausted_prime = !layout_params.filter_to_inflections_and_flats
+
+    openings = build_openings()
+    while true
+      # openings = build_openings()
+      num_changed = 0
+
+      unstable_bodies = get_unstable_bodies(cleanup_stability)
+
+      if layout_params.restack_top && unstable_bodies.length > 0 && iterations < 1
+        unstable_bodies = unstable_bodies.concat get_highest_bodies()
+
+      # this is unneeded except in testing
+      if layout_params.verbose && histo_layout_explorer_options.show_explorer
+        occupancy = build_occupancy_map()
+
+      # remove these unstable bodies from the map so that its position within the current
+      # body might be selected
+      for body, i in unstable_bodies
+        imprint_body_on_map openings, body, -1, Math.round(body.radius * cleanup_overlap) - 1
+
+        if layout_params.verbose && histo_layout_explorer_options.show_explorer
+          imprint_body_on_map occupancy, body, -1, body.radius
+
+      for body, i in unstable_bodies
+        r = body.radius
+        x = Math.round(body.position.x)
+        y = Math.round(body.position.y)
+        x_target = Math.round(body.x_target)
+
+        move_within = Math.round width * .125
+        bounds = [x_target - move_within, x_target + move_within]
+        if bounds[0] > x
+          bounds[0] = x         # allow it to stay in the same row, even though it
+        else if bounds[1] < x   # is targetting a ways away
+          bounds[1] = x
+
+        options = filter_openings openings, body.radius, !exhausted_prime
+        candidates = []
+        for col in [bounds[0]..bounds[1]]
+          continue if col < r || col >= width - r
+          if options[col] > 0
+            score = Math.log(options[col] + 1) / Math.abs( (body.x_target - col) / width )
+            candidates.push [col, options[col], score]
+        #########
+
+        candidates.sort (a,b) -> b[2] - a[2]
+
+        top_candidate = candidates[0]
+        if top_candidate && (top_candidate[0] != x || top_candidate[1] != y)
+          # wake it up and set a new position
+          if body.isSleeping
+            Matter.Sleeping.set(body, false)
+          body.sleepCounter = 0
+          body.moved_insignificantly_after_sleep = 0
+
+          Matter.Body.setPosition body, {x: top_candidate[0], y: top_candidate[1]}
+
+          if layout_params.verbose && histo_layout_explorer_options.show_explorer
+            current_cleanup.push
+              body: JSON.parse JSON.stringify {position: body.position, radius: body.radius, x_target: body.x_target, index: body.index}
+              from: {x, y}
+              to: {x: body.position.x, y: body.position.y}
+              candidates: candidates
+              openings: JSON.parse JSON.stringify openings # build_openings()
+              occupancy: JSON.parse JSON.stringify occupancy
+              prime_positions: JSON.parse JSON.stringify options
+              unstable_bodies: JSON.parse JSON.stringify ({index: b.index, position: b.position, radius: b.radius} for b in unstable_bodies)
+              bodies: ({neighbors: b.neighbors, position: b.position, radius: b.radius, index: b.index} for b in bodies)
+
+          num_changed += 1
+
+        imprint_body_on_map openings, body, 1, Math.round(body.radius * cleanup_overlap) - 1
+        if layout_params.verbose && histo_layout_explorer_options.show_explorer
+          imprint_body_on_map occupancy, body, 1, r
+
+
+      # console.log "Changed #{num_changed} positions"
+      if num_changed == 0 || iterations > 100
+        if exhausted_prime
+          # console.log "Done!", {iterations, unstable_bodies, candidates}
+          break
+        else
+          exhausted_prime = true
+          iterations = 0
+
+      find_neighbors()
+      iterations += 1
+    if layout_params.verbose && current_cleanup.length > 0
+      running_state.cleanup.push current_cleanup
+
+
+  # Returns array representing a 2d width x height map of which pixels are occupied
+  # by avatars. Each cell has the number of avatars occupying that space.
+  avatar_mask = {} # keys are radii
+  circle_adjustment = {} # keys are radii
+  build_occupancy_map = (r_mult = 1) ->
+    size = width * height
+    occupancy_map = new Int32Array width * height
+
+    for body in bodies
+      if r_mult > 1
+        r = Math.round(body.radius * r_mult) - 1
+      else
+        r = body.radius
+
+      # build a mask for the square around the body that shows which pixels
+      # in the square are covered by the circle
+      mask = avatar_mask[r]
+      if !mask
+        mask = avatar_mask[r] = new Int32Array 4 * r * r
+        col = row = 0
+        while row < 2 * r
+          while col < 2 * r
+            if Math.sqrt( ( r - .5 - row) * ( r - .5 - row ) + ( r - .5 - col ) * ( r - .5 - col ) ) < r
+              mask[ row * 2 * r + col ] = 1    # the .5 in the distance calculation is to move to the true center of the circle
+            col += 1
+          row += 1
+          col = 0
+
+      imprint_body_on_map occupancy_map, body, 1, r
+
+    occupancy_map
+
+  imprint_body_on_map = (occupancy_map, body, increment = 1, radius = null, v) ->
+    # imprint the body's mask on the occupancy map
+    # get the start offset of the occupancy map at which to start imprinting
+    r = Math.round(radius or body.radius)
+    x = Math.round body.position.x
+    y = Math.round body.position.y
+    mask = avatar_mask[r]
+    row = y - r
+    col = x - r
+    mask_row = mask_col = 0
+    while mask_row < 2 * r
+      offset_row = row + mask_row
+
+      if offset_row >= 0 && offset_row < height
+        while mask_col < 2 * r
+          offset_col = col + mask_col
+
+          if offset_col > 0 && offset_col < width && mask[ mask_row * 2 * r + mask_col ]
+            # imprint
+            occupancy_map[offset_row * width + offset_col] += increment
+            # console.log "Set #{offset_row * width + offset_col} to #{occupancy_map[offset_row * width + offset_col]}"
+
+          mask_col += 1
+
+
+      mask_row += 1
+      mask_col = 0
+
+  # An opening is a place where a body could move. Computed using the occupancy map, just doubling
+  # the radius of all bodies. Any entry in the occupancy map that remains zero is free.
+  # Then scan through and set as a valid spot to move to.
+  build_openings = ->
+    build_occupancy_map(cleanup_overlap)
+
+
+
+  # Determines which bodies are adjacent to a body. For each body, sets the angle {0, 360º} which represents
+  # the angle at which the body is touched by the other body.
+  # 0 is the right, 90º is the middle bottom, 180º is the left, 270º is the middle top.
+  #
+  # epsilon is a little extra that will help bodies that are *almost* touching to be considered touching.
+
+  a90  = Math.PI / 2
+  a180 = Math.PI
+  a270 = 3 * Math.PI / 2
+  a360 = 2 * Math.PI
+  neighbors_last_set = -1
+  find_neighbors = (epsilon = 1) ->
+    n = bodies.length
+
+    for body in bodies
+      body.neighbors = []
+
+    i = 0
+    while i < n
+      j = i + 1
+      a = bodies[i]
+
+      while j < n
+        b = bodies[j]
+
+        if b.position.y < a.position.y || (b.position.y == a.position.y && a.position.x > b.position.x)
+          aa = bodies[j]; bb = bodies[i] # make sure a is always at least as high as b; for ease in computing angles
+        else
+          aa = bodies[i]; bb = bodies[j]
+
+        a_x = aa.position.x; a_y = aa.position.y
+        b_x = bb.position.x; b_y = bb.position.y
+        a_r = aa.radius; b_r = bb.radius
+
+        dist = Math.sqrt (a_x - b_x) * (a_x - b_x) + (a_y - b_y) * (a_y - b_y)
+        if dist <= a_r + b_r + epsilon
+
+          if a_y == b_y
+            a_angle = 0
+            b_angle = a180
+          else if a_x == b_x
+            a_angle = a90
+            b_angle = a270
+          else
+            angle = Math.atan( Math.abs(a_x - b_x) / Math.abs(a_y - b_y) )
+            if a_x < b_x
+              a_angle = a90 - angle
+              b_angle = a180 + (a90 - angle)
+            else
+              a_angle = a90 + angle
+              b_angle = a360 - (a90 - angle)
+
+          aa.neighbors.push [bb.index, a_angle]
+          bb.neighbors.push [aa.index, b_angle]
+
+        j += 1
+      i += 1
+
+    neighbors_last_set = total_ticks
+
+
+  ############################
+  # Each time step, move each avatar toward its desired x-target.
+  # The default gravity in the layout_params engine is also enabled, so
+  # each avatar also has a downwards force applied.
+
+  engine.world.gravity.scale = layout_params.gravity_scale or .000012
+                                        # much less gravity than default (.001); if it is too high,
+                                        # avatars are compacted on top of each other.
+
+  x_force_mult = layout_params.x_force_mult or .006
+  beforeTick = ->
+
+    for body in bodies
+      continue if body.isStatic || body.isSleeping
+
+      # Move the avatar toward the target with force proportional
+      # to its distance away from it.
+      dist_to_target = body.x_target - body.position.x
+
+      x_push = dist_to_target * x_force_mult
+
+      ################
+      # Attempted optimization: if the avatar strings together a series of unblocked
+      # movements, boost their speed, as it suggests they may be unimpeded.
+      # Helps especially with convergence on histograms with few avatars.
+      if layout_params.enable_boosting
+        body.last_push ?= x_push
+        boostable = Math.abs(dist_to_target + body.last_push - body.last_dist_target) < .0000000000001
+        # console.log boostable, Math.abs(dist_to_target + body.last_push - body.last_dist_target)
+        if boostable
+          if body.boost_streak > 0
+            # console.log "boosted"
+            x_push = x_push * body.boost_streak
+          body.boost_streak += 1
+        else
+          x_push = x_push
+          body.boost_streak = 0
+        body.last_push = x_push
+        body.last_dist_target = dist_to_target
+      ################
+
+      # Apply the push toward the desired x-target
+      Matter.Body.translate body, {x: x_push, y: 0}
+
+
+
+
+  ticks_since_last_cleanup = 100
+  current_cleanup_stability = layout_params.cleanup_stability
+  afterTick = (ev) ->
+
+    Matter.Sleeping._motionSleepThreshold += motionSleepThresholdIncrement
+
+    total_sleeping = 0
+    for body,i in bodies
+      x = body.position.x
+      y = body.position.y
+      x_target = body.x_target
+
+      # Don't let bodies fall through the bottom
+      if y > height
+        Matter.Body.setPosition body, {x: x, y: height}
+        y = height
+
+      ##############
+      # wake bodies up periodically to see if space opened up for them while they were sleeping
+      if wake_every_x_ticks && total_ticks % wake_every_x_ticks == wake_every_x_ticks - 1
+        if body.isSleeping
+
+          # Check how much this body moved since the last time it was woke. If it didn't move much,
+          # then put it to sleep quicker this time.
+          if layout_params.reduce_sleep_threshold_if_little_movement
+            reduction_exponent = layout_params.sleep_reduction_exponent or .7
+            body.moved_insignificantly_after_sleep ?= 0
+            if body.before_sleep
+              xs = body.position.x
+              xy = body.position.y
+              dist = Math.sqrt( (x-xs) * (x-xs) + (y-xy) * (y-xy)  )
+              if dist < .00000001
+                body.moved_insignificantly_after_sleep += 1
+              else if body.moved_insignificantly_after_sleep > 0
+                body.moved_insignificantly_after_sleep = 0
+
+            body.sleepThreshold = body.original_sleep_threshold * Math.pow(reduction_exponent, (1 + body.moved_insignificantly_after_sleep))
+
+            body.before_sleep =
+              x: x
+              y: y
+
+          Matter.Sleeping.set(body, false)
+
+          # Give it a little nudge down after it awakens
+          body.force.y += 2 * body.mass * engine.world.gravity.y * engine.world.gravity.scale
+      ##########################
+
+
+      if body.isSleeping
+        total_sleeping += 1
+
+
+    #############################
+    # Swap positions when advantageous. Don't do it every tick for performance reasons, as this is n^2.
+    global_position_swap = global_swap_every_x_ticks && total_ticks % global_swap_every_x_ticks == global_swap_every_x_ticks - 1
+    if global_position_swap
+      sweep_all_for_position_swaps()
+    ##########
+
+    if layout_params.cleanup_layout_every_x_ticks
+      if total_sleeping > bodies.length * layout_params.cleanup_when_percent_sleeping && ticks_since_last_cleanup > layout_params.cleanup_layout_every_x_ticks
+
+        if current_cleanup_stability > 0
+          cleanup_layout(current_cleanup_stability)
+          if layout_params.change_cleanup_stability?
+            current_cleanup_stability += layout_params.change_cleanup_stability
+          if global_swap_every_x_ticks
+            sweep_all_for_position_swaps()
+        ticks_since_last_cleanup = 0
+      else
+        ticks_since_last_cleanup += 1
+
+    # stop when all the avatars are sleeping or we've given up
+    done = total_ticks > 2000 || total_sleeping / bodies.length >= layout_params.end_sleep_percent
+
+    done ||= opts.abort?()
+
+    if done
+
+      if layout_params.cleanup_layout_every_x_ticks
+        if layout_params.final_cleanup_stability
+          current_cleanup_stability = layout_params.final_cleanup_stability
+
+        if current_cleanup_stability > 0
+          cleanup_layout(current_cleanup_stability)
+
+      if global_swap_every_x_ticks
+        sweep_all_for_position_swaps()
+
+      Matter.Runner.stop(runner)
+      runner.enabled = false
+
+    total_ticks += 1
+
+  Matter.Runner.run = (runner, engine) ->
+
+    update_verbose = (tick_time) ->
+      running_state.layout_time ?= 0
+      running_state.layout_time += tick_time
+      global_running_state[layout_params.param_hash].tick_time += tick_time
+
+      find_neighbors()
+      evaluate_layout()
+      running_state.occupancy_map = build_occupancy_map()
+      running_state.openings = build_openings()
+      running_state.bodies = ( {index: b.index, position: b.position, radius: b.radius, neighbors: b.neighbors} for b in bodies )
+      running_state.ticks = total_ticks
+      _.extend running_state,
+        '%_sleeping': "#{(100 * total_sleeping / bodies.length).toFixed(1)}%"
+        ticks: total_ticks
+
+      save running_state
+      save global_running_state
+
+
+    after_run = ->
+      write_positions()
+      histo_run_next_job(opts)
+
+    if layout_params.show_histogram_physics
+      renderer = (time) ->
+        runner.frameRequestId = _requestAnimationFrame(renderer)
+
+        if time && runner.enabled
+          if layout_params.verbose
+            t0 = performance.now()
+
+          beforeTick()
+          Engine.update(engine)
+          afterTick()
+
+          if layout_params.verbose
+            update_verbose performance.now() - t0
+
+          write_positions()
+
+        if !runner.enabled
+          after_run()
+
+      renderer()
+
+    else
+      setTimeout ->
+
+        if layout_params.verbose
+          t0 = performance.now()
+
+        # console.profile(opts.running_state)
+        while runner.enabled
+          beforeTick()
+          Engine.update(engine)
+          afterTick()
+        # console.profileEnd(opts.running_state)
+
+        if layout_params.verbose
+          update_verbose performance.now() - t0
+
+        after_run()
+      , 0
+
+    runner
+
+
+  if layout_params.verbose
+    # console.profile(running_state.key)
+
+    running_state = fetch opts.running_state
+    global_running_state = fetch 'histo-timing'
+
+    global_running_state[layout_params.param_hash] ?=
+      tick_time: 0
+      truth: 0
+      visibility: 0
+      stability: 0
+      cnt: 0
+
+  Matter.Runner.run runner, engine
+
+
+
+
+#####
+# Calculate node radius based on the largest density of avatars in an
+# area (based on a moving average of # of opinions, mapped across the
+# width and height)
+calculateAvatarRadius = (width, height, opinions, {fill_ratio}) ->
+
+  fill_ratio ?= .25
+
+  filter_out = fetch 'filtered'
+  if filter_out.users && !filter_out.enable_comparison
+    opinions = (o for o in opinions when !(filter_out.users?[o.user]))
+
+  opinions.sort (a,b) -> a.stance - b.stance
+
+  # find most dense region of opinions
+  window_size = .05
+  avg_inc = .025
+  max_opinions = 0
+  idx = 0
+  stance = -1.0
+
+  while stance <= 1.0 + window_size
+
+    o = idx
+    cnt = 0
+    while o < opinions.length
+
+      if opinions[o].stance < stance - window_size
+        idx = o
+      else if opinions[o].stance > stance + window_size
+        break
+      else
+        cnt += 1
+
+      o += 1
+
+    if cnt > max_opinions
+      max_opinions = cnt
+
+    stance += avg_inc
+
+
+  # Calculate the avatar radius we'll use. It is based on
+  # trying to fill fill_ratio of the densest area of the histogram
+  if max_opinions > 0
+    effective_width = width * 2 * window_size
+    area_per_avatar = fill_ratio * effective_width * height / max_opinions
+    r = Math.sqrt(area_per_avatar) / 2
+
+  else
+    r = Math.sqrt(width * height / opinions.length * fill_ratio) / 2
+
+  r = Math.min(r, width / 2, height / 2)
+
+  Math.round r
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+######
+# Uses a d3-based layout_params simulation to calculate a reasonable layout
+# of avatars within a given area.
+positionAvatarsWithD3 = (opts) ->

   # Check if system energy would be reduced if two nodes' positions would
   # be swapped. We square the difference in order to favor large differences
@@ -856,10 +2217,10 @@
           if p1.y < p2.y
             p1.y -= offset_y / 2
             p2.y += offset_y / 2
+
           else
             p2.y -= offset_y / 2
             p1.y += offset_y / 2
-

       # Visit subregions if we could possibly have a collision there
@@ -875,78 +2236,105 @@
               y2 < ny1

   write_positions = ->
-    positions = {}
-    for o, i in nodes
-      positions[parseInt(opinions[i].user.substring(6))] = \
-        [Math.round((o.x - o.radius) * 10) / 10, Math.round((o.y - o.radius) * 10) / 10]
-
-    opts.done?(positions)
-
-  ##############
-  # Initialize positions of each node
-  targets = {}
-  opinions = opts.o.slice()
-  width = opts.w or 400
-  height = opts.h or 70
-  r = calculateAvatarRadius width, height, opinions
-
-  nodes = opinions.map (o, i) ->
-    x_target = (o.stance + 1) / 2 * width
-
-    if targets[x_target]
-      if x_target > .98
-        x_target -= .1 * Math.random()
-      else if x_target < .02
-        x_target += .1 * Math.random()
-
-    targets[x_target] = 1
-
-    x = x_target
-    y = height - r
-
-    return {
-      index: i
-      radius: r
-      x: x
-      y: y
-      x_target: x_target
-    }
+    setTimeout ->
+      positions = {}
+      for o, i in nodes
+        positions[parseInt(opinions[i].user.substring(6))] = \
+          [Math.round((o.x - o.radius) * 10) / 10, Math.round((o.y - o.radius) * 10) / 10, false, o.stability]
+
+      opts.done?(positions)
+
+  {nodes, targets, opinions, width, height, r} = initialize_positions opts
+  bodies = nodes
+  layout_params = opts.layout_params

   ###########
   # run the simulation
   stable = false
-  alpha = .8
-  decay = .8
-  min_alpha = 0.0000001
-  x_force_mult = 2
-  y_force_mult = 2
+  alpha = layout_params.alpha or .8
+  decay = layout_params.decay or .8
+  min_alpha = layout_params.min_alpha or 0.0000001
+  x_force_mult = layout_params.x_force_mult or 2
+  y_force_mult = layout_params.y_force_mult or 2

   total_ticks = 0
   collisions = 0

-  loc = fetch 'location'
-  if loc.query_params.show_histogram_physics
+  if layout_params.verbose
+    # console.profile(running_state.key)
+
+    running_state = fetch opts.running_state
+    global_running_state = fetch 'histo-timing'
+
+    global_running_state[layout_params.param_hash] ?=
+      tick_time: 0
+      truth: 0
+      visibility: 0
+      stability: 0
+      cnt: 0
+
+
+  update_verbose = (tick_time) ->
+    running_state.layout_time ?= 0
+    running_state.layout_time += tick_time
+    global_running_state[layout_params.param_hash].tick_time += tick_time
+    global_running_state[layout_params.param_hash].cnt += 1
+
+    running_state.ticks = total_ticks
+    _.extend running_state,
+      ticks: total_ticks
+
+    save running_state
+    save global_running_state
+
+  after_run = ->
+    write_positions()
+    histo_run_next_job(opts)
+
+  if opts.layout_params?.show_histogram_physics
+
+    _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame \
+                                  || window.mozRequestAnimationFrame || window.msRequestAnimationFrame \
+                                  || (callback) ->
+                                        setTimeout ->
+                                          callback()
+                                        , 1000 / 60
+
     iterate = =>
+      if layout_params.verbose
+        t0 = performance.now()
+
       stable = tick alpha
+
       alpha *= decay
       total_ticks += 1

-      console.log "Tick: #{total_ticks} Collisions: #{collisions}"
+      # console.log "Tick: #{total_ticks} Collisions: #{collisions}"
       collisions = 0

       stable ||= alpha <= min_alpha

-      if !aborted
-        write_positions()
+
+      if layout_params.verbose
+        update_verbose performance.now() - t0

       aborted = opts.abort?()
       if stable || aborted
-        histo_run_next_job(opts)
+        # console.profileEnd(opts.running_state)
+        after_run()
+
       else
-        setTimeout iterate, loc.query_params.show_histogram_physics or 100
+        write_positions()
+        _requestAnimationFrame iterate
+
+
     iterate()

   else
+
+    if layout_params.verbose
+      t0 = performance.now()
+
     while true
       stable = tick alpha
       alpha *= decay
@@ -957,96 +2345,10 @@
       aborted = opts.abort?()
       break if stable || aborted

-
-    if !aborted
-      write_positions()
-
-    histo_run_next_job(opts)
-
-
-
-#####
-# Calculate node radius based on the largest density of avatars in an
-# area (based on a moving average of # of opinions, mapped across the
-# width and height)
-
-calculateAvatarRadius = (width, height, opinions) ->
-  filter_out = fetch 'filtered'
-  if filter_out.users && !filter_out.enable_comparison
-    opinions = (o for o in opinions when !(filter_out.users?[o.user]))
-
-  opinions.sort (a,b) -> a.stance - b.stance
-
-  # first, calculate a moving average of the number of opinions
-  # across around all possible stances
-  window_size = .3
-  avg_inc = .01
-  moving_avg = []
-  idx = 0
-  stance = -1.0
-  sum = 0
-
-  while stance <= 1.0
-
-    o = idx
-    cnt = 0
-    while o < opinions.length
-
-      if opinions[o].stance < stance - window_size
-        idx = o
-      else if opinions[o].stance > stance + window_size
-        break
-      else
-        cnt += 1
-
-      o += 1
-
-    moving_avg.push cnt
-    stance += avg_inc
-    sum += cnt
-
-  # second, calculate the densest area of opinions, operationalized
-  # as the region with the most opinions amongst all regions of
-  # opinion space that have contiguous above average opinions.
-  dense_regions = []
-  avg_of_moving_avg = sum / moving_avg.length
-
-  current_region = []
-  for avg, idx in moving_avg
-    reset = idx == moving_avg.length - 1
-    if avg >= avg_of_moving_avg
-      current_region.push idx
-    else
-      reset = true
-
-    if reset && current_region.length > 0
-      dense_regions.push [current_region[0] * avg_inc - 1.0 - window_size , \
-                    idx * avg_inc - 1.0 + window_size ]
-      current_region = []
-
-  max_region = null
-  max_opinions = 0
-  for region in dense_regions
-    cnt = 0
-    for o in opinions
-      if o.stance >= region[0] && \
-         o.stance <= region[1]
-        cnt += 1
-    if cnt > max_opinions
-      max_opinions = cnt
-      max_region = region
-
-  # Third, calculate the avatar radius we'll use. It is based on
-  # trying to fill ratio_filled of the densest area of the histogram
-  ratio_filled = .5
-  if max_opinions > 0
-    effective_width = width * Math.abs(max_region[0] - max_region[1]) / 2
-    area_per_avatar = ratio_filled * effective_width * height / max_opinions
-    r = Math.sqrt(area_per_avatar) / 2
-  else
-    r = Math.sqrt(width * height / opinions.length * ratio_filled) / 2
-
-  r = Math.min(r, width / 2, height / 2)
-
-  r
-
+    if layout_params.verbose
+      update_verbose performance.now() - t0
+
+    after_run()
+
+
+


special-case comment

special-case comment

--- src/components/performance.cjsx
+++ src/components/performance.cjsx
@@ -20,6 +20,7 @@
     isNameSort: true

   sortClick: (event) ->
+    # this is a special case for the name header data which is one level above nested data
     if not _.contains(event.target.classList, 'student-name')
       @setState({isNameSort: false})
     else


Fixed eating parameters by convertor

Fixed eating parameters by convertor

--- src/convert-ast-metadata.coffee
+++ src/convert-ast-metadata.coffee
@@ -2,10 +2,11 @@
   result = {}
   if Array.isArray metadata
     for item in metadata
-      name = item['name']
-      delete item['name']
+      dupItem = JSON.parse(JSON.stringify(item))
+      name = dupItem['name']
+      delete dupItem['name']

-      result[name] = item
+      result[name] = dupItem

   result


findURL fixes

findURL fixes

--- src/utils/find-url.coffee
+++ src/utils/find-url.coffee
@@ -6,17 +6,19 @@
   return unless domWalker.isNode(sourceNode)
   body = document.body
   node = sourceNode
-  while node != body
-    node = node.parentNode
-
+  while node and node != body
     # Find link on self
-    link = domWalker.findLink(node)
-
-    # or, find first link from siblings
-    link ||= _.find domWalker.getSiblings(node), domWalker.findLink
+    # or find first link from siblings
+    link = domWalker.getLinkValue(node)
+    unless link
+      matchingNode = _.find(domWalker.getSiblings(node), domWalker.getLinkValue)
+      link =  domWalker.getLinkValue matchingNode

     # If we found something, return.
     return link if link?
+
+    # Else, walk up
+    node = node.parentNode

 getPageUrl = ()->
   window.location.href


Fixed the call optimization

Fixed the call optimization

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -4390,9 +4390,9 @@
   first = (j) -> varNames[0...j].join ', '
   # TODO: handle A9 first branch
   """var _#{i} = function (fn, #{first i}) {
-    if (fn._ === #{i} || fn.length === #{i}) {
+    if (fn._ === #{i} || !fn._ && fn.length === #{i}) {
       return fn(#{first i});
-    } else if (fn._ > #{i} || fn.length > #{i}) {
+    } else if (fn._ > #{i} || !fn._ && fn.length > #{i}) {
       return function (#{varNames[i]}) {
         return _#{i + 1}(fn, #{first i + 1});
       };


Finally simplified deferring to be only inside nameCompile

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -802,12 +802,7 @@

   paramNames = ctx.arity operator.symbol
   if not paramNames
-    # log "deferring in known call #{operator.symbol}"
-    ctx.doDefer operator, operator.symbol
-    if ctx.assignTo()
-      return {}
-    else
-      return assignCompile ctx, call, deferredExpression()
+    return callUnknownCompile ctx, call
   paramNamesSet = arrayToSet paramNames
   positionalParams = filter ((param) -> not (lookupInMap labeledArgs, param)), paramNames
   nonLabeledArgs = map _snd, filter (([label, value]) -> not label), args
@@ -915,7 +910,6 @@

 callUnknownTranslate = (ctx, translatedOperator, call) ->
   args = _arguments call
-

   argList = if ctx.shouldDefer()
     deferredExpression()
@@ -2942,7 +2936,9 @@
       nameTranslate ctx, atom, symbol, type
     else
       # log "deferring in rhs for #{symbol}", ctx._deferrableDefinition().name
+      # type = toConstrained ctx.freshTypeVariable star
       ctx.doDefer atom, symbol
+      # type: type
       translation: deferredExpression()

 scopedName = (name, scopeIndex) ->


Show error message if user add failed.

Show error message if user add failed.

--- app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee
+++ app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee
@@ -298,12 +298,12 @@
   headPrefix: 'New'

   content: ->
-    controller = new App.ControllerForm(
+    @controller = new App.ControllerForm(
       model:     App.User
       screen:    'edit'
       autofocus: true
     )
-    controller.form
+    @controller.form

   onSubmit: (e) =>
     params = @formParam(e.target)
@@ -324,7 +324,7 @@
     errors = user.validate()
     if errors
       @log 'error', errors
-      @formValidate( form: e.target, errors: errors )
+      @formValidate(form: e.target, errors: errors)
       return

     # save user
@@ -334,13 +334,15 @@

         # force to reload object
         callbackReload = (user) ->
-          ui.parent.el.find('[name=customer_id]').val( user.id ).trigger('change')
+          ui.parent.el.find('[name=customer_id]').val(user.id).trigger('change')
           ui.parent.close()

           # start customer info controller
           ui.close()
         App.User.full(@id, callbackReload , true)

-      fail: ->
-        ui.close()
-    )
+      fail: (settings, details) ->
+        ui.log 'errors', details
+        ui.formEnable(e)
+        ui.controller.showAlert(details.error_human || details.error || 'Unable to create object!')
+    )


Classroom: Prefetch next course on course submit.

--- client/app/Applications/Classroom.kdapplication/Views/classroomworkspace.coffee
+++ client/app/Applications/Classroom.kdapplication/Views/classroomworkspace.coffee
@@ -155,4 +155,10 @@
     if container.hasClass "active"
       KD.getSingleton("windowController").addLayer container
       container.once "ReceivedClickElsewhere", ->
-        container.toggleClass "active"+        container.toggleClass "active"
+
+  next: ->
+    super
+    delegate = @getDelegate()
+    delegate.currentChapterIndex++
+    delegate.fetchNextCourseConfig()


Splitted more methods and renamed private functions

--- lib/oauth1.coffee
+++ lib/oauth1.coffee
@@ -32,15 +32,6 @@

 OAuthBase = require './oauth-base'

-replace_param = (param, params, hard_params, keyset) ->
-	param = param.replace /\{\{(.*?)\}\}/g, (match, val) ->
-		return hard_params[val] || ""
-	return param.replace /\{(.*?)\}/g, (match, val) ->
-		return "" if ! params[val] || ! keyset[val]
-		if Array.isArray(keyset[val])
-			return keyset[val].join params[val].separator || ","
-		return keyset[val]
-
 class OAuth1 extends OAuthBase
 	constructor: ->
 		@_params = {}
@@ -48,6 +39,15 @@

 	_setParams: (parameters) ->
 		@_params[k] = v for k,v of parameters
+
+	_replace_param: (param, params, hard_params, keyset) ->
+		param = param.replace /\{\{(.*?)\}\}/g, (match, val) ->
+			return hard_params[val] || ""
+		return param.replace /\{(.*?)\}/g, (match, val) ->
+			return "" if ! params[val] || ! keyset[val]
+			if Array.isArray(keyset[val])
+				return keyset[val].join params[val].separator || ","
+			return keyset[val]

 	authorize: (provider, parameters, opts, callback) ->
 		@_setParams(provider.parameters)
@@ -61,20 +61,20 @@
 			options: opts.options,
 			expire: 600
 		dbstates.add data, (err, state) =>
-			@dbstatesAddedCallback(err, state, provider, parameters, opts, callback)
-
-	dbstatesAddedCallback: (err, state, provider, parameters, opts, callback) ->
+			@_dbAddedCallback(err, state, provider, parameters, opts, callback)
+
+	_dbAddedCallback: (err, state, provider, parameters, opts, callback) ->
 		request_token = provider.oauth1.request_token
 		query = {}
 		if typeof opts.options?.request_token == 'object'
 			query = opts.options.request_token
 		for name, value of request_token.query
-			param = replace_param value, @_params, state:state.id, callback:config.host_url+config.relbase, parameters
+			param = @_replace_param value, @_params, state:state.id, callback:config.host_url+config.relbase, parameters
 			query[name] = param if param
 		headers = {}
 		headers["Accept"] = short_formats[request_token.format] || request_token.format if request_token.format
 		for name, value of request_token.headers
-			param = replace_param value, @_params, {}, parameters
+			param = @_replace_param value, @_params, {}, parameters
 			headers[name] = param if param
 		options =
 			url: request_token.url
@@ -92,24 +92,27 @@
 			options.qs = query

 		# do request to request_token
-		request options, (e, r, body) ->
+		request options, (e, r, body) =>
 			return callback(e) if e
 			responseParser = new OAuth1ResponseParser(r, body, headers["Accept"], 'request_token')
 			return callback(responseParser.error) if responseParser.error

-			dbstates.setToken state.id, responseParser.oauth_token_secret, (e, r) ->
-				return callback e if e
-				authorize = provider.oauth1.authorize
-				query = {}
-				if typeof opts.options?.authorize == 'object'
-					query = opts.options.authorize
-				for name, value of authorize.query
-					param = replace_param value, @_params, state:state.id, callback:config.host_url+config.relbase, parameters
-					query[name] = param if param
-				query.oauth_token = responseParser.oauth_token
-				url = replace_param authorize.url, @_params, {}, parameters
-				url += "?" + querystring.stringify query
-				callback null, url
+			dbstates.setToken state.id, responseParser.oauth_token_secret, (e, r) =>
+				@_dbSetTokenCallback(e, r, state, provider, parameters, opts, responseParser, callback)
+
+	_dbSetTokenCallback: (e, r, state, provider, parameters, opts, responseParser, callback) ->
+		return callback e if e
+		authorize = provider.oauth1.authorize
+		query = {}
+		if typeof opts.options?.authorize == 'object'
+			query = opts.options.authorize
+		for name, value of authorize.query
+			param = @_replace_param value, @_params, state:state.id, callback:config.host_url+config.relbase, parameters
+			query[name] = param if param
+		query.oauth_token = responseParser.oauth_token
+		url = @_replace_param authorize.url, @_params, {}, parameters
+		url += "?" + querystring.stringify query
+		callback null, url

 	access_token: (state, req, callback) ->
 		if not req.params.oauth_token && not req.params.error
@@ -145,15 +148,15 @@
 			for extra in (provider.oauth1.authorize.extra||[])
 				hard_params[extra] = req.params[extra] if req.params[extra]
 			for name, value of access_token.query
-				param = replace_param value, @_params, hard_params, parameters
+				param = @_replace_param value, @_params, hard_params, parameters
 				query[name] = param if param
 			headers = {}
 			headers["Accept"] = short_formats[access_token.format] || access_token.format if access_token.format
 			for name, value of access_token.headers
-				param = replace_param value, @_params, {}, parameters
+				param = @_replace_param value, @_params, {}, parameters
 				headers[name] = param if param
 			options =
-				url: replace_param access_token.url, @_params, hard_params, parameters
+				url: @_replace_param access_token.url, @_params, hard_params, parameters
 				method: access_token.method?.toUpperCase() || "POST"
 				oauth:
 					callback: query.oauth_callback
@@ -224,13 +227,13 @@
 			if options.url[0] != '/'
 				options.url = '/' + options.url
 			options.url = oauthrequest.url + options.url
-		options.url = replace_param options.url, params, parameters.oauthio, parameters
+		options.url = @_replace_param options.url, params, parameters.oauthio, parameters

 		# build query
 		options.qs = {}
 		options.qs[name] = value for name, value of req.query
 		for name, value of oauthrequest.query
-			param = replace_param value, params, parameters.oauthio, parameters
+			param = @_replace_param value, params, parameters.oauthio, parameters
 			options.qs[name] = param if param

 		options.oauth =
@@ -246,7 +249,7 @@
 			'accept-language':req.headers['accept-language']
 			'content-type':req.headers['content-type']
 		for name, value of oauthrequest.headers
-			param = replace_param value, params, parameters.oauthio, parameters
+			param = @_replace_param value, params, parameters.oauthio, parameters
 			options.headers[name] = param if param

 		# build body


Update shipping-rate-selection directive to handle error when retreiving rates

--- app/assets/javascripts/sprangular/directives/shippingRatesSelection.coffee
+++ app/assets/javascripts/sprangular/directives/shippingRatesSelection.coffee
@@ -36,3 +36,6 @@
                 order.shipping_method_id = results[0].shippingMethodId

               $scope.loading = false
+            .error ->
+              $scope.rates = []
+              $scope.loading = false


Make usernames available in project overview so usernames can be shown.

--- lib/router.coffee
+++ lib/router.coffee
@@ -2,11 +2,11 @@
   layoutTemplate: 'layout'
   loadingTemplate: 'loading'
   notFoundTemplate: 'notFound'
-  waitOn: -> [Meteor.subscribe('projects'), Meteor.subscribe('notifications')]
+  waitOn: -> [Meteor.subscribe('projects'),
+              Meteor.subscribe('notifications'),
+              Meteor.subscribe('usernames')]

-Router.route '/projects/new',
-  name: 'projectNew'
-  waitOn: -> Meteor.subscribe 'usernames'
+Router.route '/projects/new', name: 'projectNew'

 Router.route '/projects/:_id',
   name: 'projectPage'
@@ -16,8 +16,7 @@
     [Meteor.subscribe('measures', projectId),
      Meteor.subscribe('risks', projectId),
      Meteor.subscribe('findings', projectId),
-     Meteor.subscribe('sources', projectId),
-     Meteor.subscribe('usernames')]
+     Meteor.subscribe('sources', projectId)]

 Router.route '/', name: 'projectsList'


Clean up app.config directives

Clean up app.config directives

--- lib/server.coffee
+++ lib/server.coffee
@@ -51,21 +51,14 @@
   # static files
   app.configure 'development', ->
     logger.setLevel(log4js.levels.DEBUG)
-    app.use "/static/", express.static(
-      __dirname + '/../assets')
+    app.use "/static/", express.static(__dirname + '/../assets')
+    app.use "/uploads/", express.static(__dirname + '/../uploads')
+
   app.configure 'production', ->
     logger.setLevel(log4js.levels.ERROR)
-    app.use "/static/", express.static(
-      __dirname + '/../assets',
+    app.use "/static/", express.static(__dirname + '/../assets',
       {maxAge: 1000*60*60*24})
-  app.configure 'development', ->
-    logger.setLevel(log4js.levels.DEBUG)
-    app.use "/uploads/", express.static(
-      __dirname + '/../uploads')
-  app.configure 'production', ->
-    logger.setLevel(log4js.levels.ERROR)
-    app.use "/uploads/", express.static(
-      __dirname + '/../uploads',
+    app.use "/uploads/", express.static(__dirname + '/../uploads',
       {maxAge: 1000*60*60*24})

   view_folders = [__dirname + "/../views"]


graph shows dni data

graph shows dni data

--- app/assets/javascripts/pv_queries.js.coffee
+++ app/assets/javascripts/pv_queries.js.coffee
@@ -2,49 +2,56 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http:#coffeescript.org/
 $ ->
-  alert $("#mygraph").data("dni")
+  #alert $("#mygraph").data("dni")

-data = null
-graph = null
-custom = (month, hour) ->
-  Math.sin(month/50) * Math.cos(hour/50) * 50 + 50
+  data = null
+  graph = null
+  custom = (month, hour) ->
+    Math.sin(month/50) * Math.cos(hour/50) * 50 + 50

 # Called when the Visualization API is loaded.
-drawVisualization = () ->
-  # Create and populate a data table.
-  data = new google.visualization.DataTable
-  data.addColumn('number', 'month')
-  data.addColumn('number', 'hour')
-  data.addColumn('number', 'kW')
+  drawVisualization = () ->
+    # Create and populate a data table.
+    data = new google.visualization.DataTable
+    data.addColumn('number', 'month')
+    data.addColumn('number', 'hour')
+    data.addColumn('number', 'kW')

-  # create some nice looking data with sin/cos
-  steps = 25  # number of datapoints will be steps*steps
-  axisMax = 314
-  axisStep = axisMax / steps
+    dni_string = $("#mygraph").data("dni")
+    dni_array = dni_string.split(" ")
+    for month in [1..12]
+      for hour in [0..4]
+        kW = parseFloat dni_array.shift()
+        row = [month, hour, kW]
+        data.addRow(row)

-  #data.addRow row for row in axisMax by axisStep
-  for month in [0..axisMax] by axisStep
-    for hour in [0..axisMax] by axisStep
-      kW = custom(month, hour)
-      row = [month, hour, kW]
-      data.addRow(row)
+    # create some nice looking data with sin/cos
+    #steps = 25  # number of datapoints will be steps*steps
+    #axisMax = 314
+    #axisStep = axisMax / steps
+    ##data.addRow row for row in axisMax by axisStep
+    #for month in [0..axisMax] by axisStep
+      #for hour in [0..axisMax] by axisStep
+        #kW = custom(month, hour)
+        #row = [month, hour, kW]
+        #data.addRow(row)

-  # specify options
-  options =
-    width:  "500px"
-    height: "400px"
-    style: "surface"
-    showPerspective: true
-    showGrid: true
-    showShadow: false
-    keepAspectRatio: true
-    verticalRatio: 0.5
+    # specify options
+    options =
+      width:  "500px"
+      height: "400px"
+      style: "surface"
+      showPerspective: true
+      showGrid: true
+      showShadow: false
+      keepAspectRatio: true
+      verticalRatio: 0.5

-  # Instantiate our graph object.
-  graph = new links.Graph3d(document.getElementById('mygraph'))
+    # Instantiate our graph object.
+    graph = new links.Graph3d(document.getElementById('mygraph'))

-  # Draw our graph with the created data and options
-  graph.draw(data, options)
+    # Draw our graph with the created data and options
+    graph.draw(data, options)

 # Set callback to run when API is loaded
-google.setOnLoadCallback(drawVisualization)
+  google.setOnLoadCallback(drawVisualization)


- fixes

- fixes

--- app/assets/javascripts/spree/cart.js.coffee
+++ app/assets/javascripts/spree/cart.js.coffee
@@ -168,8 +168,9 @@
     url: url
     success: (response)->
       $('#js-order-adjustments').html response
-      $(".js-hidden-radio").each  ()->
-        $('.js-special-radio', $('.shipping-method')).click()
+#      $(".js-hidden-radio").each  ()->
+#
+#        $('.js-special-radio', $('.shipping-method')).click()

 checkAddress = (value, my_this)->
   console.log value


Edit styles when no description but editable is available

--- app/coffee/modules/common/components.coffee
+++ app/coffee/modules/common/components.coffee
@@ -524,7 +524,7 @@

     """ # TODO: i18n
     noDescriptionMegEditMode = """
-

+

         No description yet, why don't you add a good one clicking here?

     """ # TODO: i18n
@@ -535,8 +535,8 @@
     """ # TODO: i18n

     link = ($scope, $el, $attrs, $model) ->
-        $el.find('div.edit-description').hide()
-        $el.find('div.view-description span.edit').hide()
+        $el.find('.edit-description').hide()
+        $el.find('.view-description .edit').hide()

         isEditable = ->
             return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
@@ -547,8 +547,8 @@
             return if not isEditable()
             target = angular.element(event.target)
             if not target.is('a')
-                $el.find('div.edit-description').show()
-                $el.find('div.view-description').hide()
+                $el.find('.edit-description').show()
+                $el.find('.view-description').hide()
                 $el.find('textarea').focus()

         $el.on "click", ".save", ->
@@ -559,8 +559,8 @@
             promise.then ->
                 $confirm.notify("success")
                 $rootscope.$broadcast("history:reload")
-                $el.find('div.edit-description').hide()
-                $el.find('div.view-description').show()
+                $el.find('.edit-description').hide()
+                $el.find('.view-description').show()
             promise.then null, ->
                 $confirm.notify("error")
             promise.finally ->
@@ -569,17 +569,17 @@
         $el.on "keyup", "textarea", ->
             if event.keyCode == 27
                 $scope.item.revert()
-                $el.find('div.edit-description').hide()
-                $el.find('div.view-description').show()
+                $el.find('.edit-description').hide()
+                $el.find('.view-description').show()

         $scope.$watch $attrs.ngModel, (value) ->
             return if not value
             $scope.item = value

             if isEditable()
-                $el.find('div.view-description span.edit').show()
+                $el.find('.view-description .edit').show()
+                $el.find('.view-description p').addClass('editable')
                 $scope.noDescriptionMsg = noDescriptionMegEditMode
-                $el.find('.view-description p').addClass('editable')
             else
                 $scope.noDescriptionMsg = noDescriptionMegReadMode


remove old console.log

remove old console.log

--- app/components/group-page/group-page.coffee
+++ app/components/group-page/group-page.coffee
@@ -18,7 +18,6 @@
           $scope.group = group
           $scope.currentUser = CurrentUser()
           $scope.membership = group.membershipFor(CurrentUser())
-          console.log('$scope.membership: ', $scope.membership)
           Records.memberships.fetchByGroupId(groupId)
           Records.buckets.fetchByGroupId(groupId).then ->
             LoadBar.stop()


when visiting group page, currentGroupId global set to the id of that group

--- app/components/group-page/group-page.coffee
+++ app/components/group-page/group-page.coffee
@@ -6,7 +6,9 @@
   template: require('./group-page.html')
   controller: ($scope, Records, $stateParams, $location, CurrentUser, Toast) ->

-    groupId = parseInt($stateParams.groupId)
+    groupId = parseInt($stateParams.groupId)
+    global.cobudgetApp.currentGroupId = groupId
+
     Records.groups.findOrFetchById(groupId).then (group) ->
       $scope.group = group
       $scope.currentMembership = group.membershipFor(CurrentUser())


Cleanup code

Cleanup code

--- src/scripts/helpers/backbone/views/base.coffee
+++ src/scripts/helpers/backbone/views/base.coffee
@@ -16,8 +16,7 @@

     appendAs: (type, view) ->
       @$el = @parent.$el
-      if @el
-        @$el = @parent.$el.find(@el)
+      if @el then @$el = @parent.$el.find(@el)
       view.parent = @parent
       @views ?= []
       @views.push(view)


remove commented line

remove commented line

--- client/app/Applications/Activity.kdapplication/ListItems/ActivityListItemBlogPost.coffee
+++ client/app/Applications/Activity.kdapplication/ListItems/ActivityListItemBlogPost.coffee
@@ -13,7 +13,6 @@
     super options,data

     @readThisLink = new CustomLinkView
-      #cssClass : 'read-this-link'
       title : @getData().title or 'Read this Blog Post'
       click : (event)=>
         event.stopPropagation()


task US title in timeline test

task US title in timeline test

--- app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee
+++ app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee
@@ -243,3 +243,37 @@
         title = mySvc.getTitle(timeline, event, type)

         expect(title).to.be.equal("title_ok")
+
+    it "task title with us_name", () ->
+        timeline = {
+            data: {
+                task: {
+                    name: 'task_name',
+                    userstory: {
+                        ref: 2
+                        subject: 'subject'
+                    }
+                }
+            }
+        }
+
+        event = {
+            obj: 'task',
+        }
+
+        type = {
+            key: 'TITLE_OBJ',
+            translate_params: ['us_name']
+        }
+
+        objparam = sinon.match ((value) ->
+            return value.us_name == '#2 subject'
+         ), "objparam"
+
+        mockTranslate.instant
+            .withArgs('TITLE_OBJ', objparam)
+            .returns('title_ok')
+
+        title = mySvc.getTitle(timeline, event, type)
+
+        expect(title).to.be.equal("title_ok")


Formatting tweak

Formatting tweak

--- app/todos-classic/src/viewmodels/app.coffee
+++ app/todos-classic/src/viewmodels/app.coffee
@@ -25,7 +25,6 @@
 	# Header Section
 	#############################
 	@title = ko.observable('')
-
 	@onAddTodo = (view_model, event) =>
 		return true if not $.trim(@title()) or (event.keyCode != ENTER_KEY)


Grid fill with item margin fixes

Grid fill with item margin fixes

--- impl/base/utils/grid.coffee
+++ impl/base/utils/grid.coffee
@@ -169,64 +169,67 @@
 	# set children positions
 	i = 0
 	for child in children
-		# omit not visible and auto positioned children
+		# omit not visible children
 		if not child._visible
 			continue
+
+		column = i % columnsLen
+		row = Math.floor(i/columnsLen) % rowsLen
+		i++
+
+		# omit auto positioned children
 		if anchors = child._anchors
 			if gridType & ROW and anchors._autoX
 				continue
 			if gridType & COLUMN and anchors._autoY
 				continue

-		column = i % columnsLen
-		row = Math.floor(i/columnsLen) % rowsLen
-
 		margin = child._margin
 		layout = child._layout

+		# get column position
 		columnX = if column > 0 then columnsPositions[column-1] else 0
+		if margin and (includeBorderMargins or (column > 0 and column < columnsLen))
+			leftMargin = margin._left
+			rightMargin = margin._right
+		else
+			leftMargin = rightMargin = 0
+
+		# get row position
 		rowY = if row > 0 then rowsPositions[row-1] else 0
-
+		if margin and (includeBorderMargins or (row > 0 and row < rowsLen))
+			topMargin = margin._top
+			bottomMargin = margin._bottom
+		else
+			topMargin = bottomMargin = 0
+
+		# set sizes
 		if layout
 			# set width
 			if layout._fillWidth and not autoWidth
-				child.width = columnsPositions[column] - columnX
+				child.width = columnsPositions[column] - columnX - leftMargin - rightMargin

 			# set height
 			if layout._fillHeight and not autoHeight
-				child.height = rowsPositions[row] - rowY
+				child.height = rowsPositions[row] - rowY - topMargin - bottomMargin

 		# set x
 		if gridType & ROW or alignH isnt 'left' or (margin and (margin._left or margin._right) and not anchors?._autoX)
-			x = columnX
-			if margin and (includeBorderMargins or (column > 0 and column < columnsLen))
-				x += margin._left
+			x = columnX + leftMargin
 			if alignH is 'center'
-				x += (columnsPositions[column] - x - child._width) / 2
-				if margin and (includeBorderMargins or (column > 0 and column < columnsLen))
-					x -= margin.right / 2
+				x += (columnsPositions[column] - x - child._width) / 2 - rightMargin / 2
 			else if alignH is 'right'
-				x += columnsPositions[column] - x - child._width
-				if margin and (includeBorderMargins or (column > 0 and column < columnsLen))
-					x -= margin.right
+				x += columnsPositions[column] - x - child._width - rightMargin
 			child.x = x

 		# set y
 		if gridType & COLUMN or alignV isnt 'top' or (margin and (margin._top or margin._bottom) and not anchors?._autoY)
-			y = rowY
-			if margin and (includeBorderMargins or (row > 0 or row < rowsLen))
-				y += margin._top
+			y = rowY + topMargin
 			if alignV is 'center'
-				y += (rowsPositions[row] - y - child._height) / 2
-				if margin and (includeBorderMargins or (row > 0 or row < rowsLen))
-					y -= margin.bottom / 2
+				y += (rowsPositions[row] - y - child._height) / 2 - bottomMargin / 2
 			else if alignV is 'right'
-				y += rowsPositions[row] - y - child._height
-				if margin and (includeBorderMargins or (row > 0 or row < rowsLen))
-					y -= margin.bottom
+				y += rowsPositions[row] - y - child._height - bottomMargin
 			child.y = y
-
-		i++

 	# set item size
 	if autoWidth


Remove party search sort

Remove party search sort

--- app/assets/party/search.coffee
+++ app/assets/party/search.coffee
@@ -4,6 +4,6 @@
   '$scope', '$filter', 'displayService', 'parties',
   ($scope, $filter, display, parties) ->
     display.title = 'Users'
-    $scope.parties = $filter('orderBy')(parties, 'lastName')
+    $scope.parties = parties
     return
 ]


Fix "required" number

Fix "required" number

--- app/classifier/classifier.cjsx
+++ app/classifier/classifier.cjsx
@@ -35,9 +35,9 @@

   renderTaskArea: (annotation, task, currentTool) ->
     onFirstTask = @props.classification.annotations.length is 1
-    needsAnswer = task.required and not annotation.answer?
+    givenAnswers = (answer for answer in [].concat annotation.answer when answer?)
+    answerStillRequired = givenAnswers.length < task.required ? 0
     nextTaskKey = annotation._answer?.next ? task.next
-    canGoForward = needsAnswer or nextTaskKey?

       <TaskViewer task={task} annotation={annotation} selectedDrawingTool={currentTool} onChange={@handleAnswer} />
@@ -45,10 +45,10 @@

         <button className="backward" disabled={onFirstTask} onClick={@previousTask}></button>

-        {if canGoForward
-          <button className="forward" disabled={needsAnswer} onClick={@loadTask.bind this, nextTaskKey}>Next </button>
+        {if nextTaskKey?
+          <button className="forward" disabled={answerStillRequired} onClick={@loadTask.bind this, nextTaskKey}>Next </button>
         else
-          <button className="forward" disabled={needsAnswer} onClick={@finishClassification}>Finished </button>}
+          <button className="forward" disabled={answerStillRequired} onClick={@finishClassification}>Finished </button>}


[i18n] Fix a strange behavior when set user language to default-sys-lang

--- app/coffee/modules/auth.coffee
+++ app/coffee/modules/auth.coffee
@@ -35,15 +35,16 @@
                  "$tgResources",
                  "$tgHttp",
                  "$tgUrls",
+                 "$tgConfig",
                  "$translate"]

-    constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @translate) ->
+    constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @config, @translate) ->
         super()

     _setLocales: ->
-        if @rootscope.user.lang
-            @translate.use(@rootscope.user.lang)
-            moment.locale(@rootscope.user.lang)
+        lang = @rootscope.user.lang || @config.get("defaultLanguage") || "en"
+        @translate.use(lang)
+        moment.locale(lang)

     getUser: ->
         if @rootscope.user


payment: vm product form should force plan upgrade

(if necessary)

--- client/landing/app/Applications/Environments.kdapplication/views/vms/vmproductform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/vms/vmproductform.coffee
@@ -1,7 +1,7 @@
 class VmProductForm extends FormWorkflow

   createUpgradeForm: ->
-    (KD.getSingleton 'paymentController').createUpgradeForm 'vm'
+    (KD.getSingleton 'paymentController').createUpgradeForm 'vm', yes

   checkUsageLimits: (pack, callback) ->
     { subscription } = @collector.data


Clean up inspect(frame.data)

Clean up inspect(frame.data)

--- src/ext/modules/routines.coffee
+++ src/ext/modules/routines.coffee
@@ -388,24 +388,18 @@
         #XXX format functions
         switch column.type
           when 'int'
-            label: column.label
-            type: TInteger
+            Flow.Data.Variable column.label, TInteger
           when 'real'
-            label: column.label
-            type: TReal
+            Flow.Data.Variable column.label, TReal
           when 'enum'
-            label: column.label
-            type: TFactor
-            domain: column.domain
+            Flow.Data.Factor column.label, column.domain
           when 'uuid', 'string'
-            label: column.label
-            type: TString
+            Flow.Data.Variable column.label, TString
           when 'time'
-            label: column.label
-            type: TDate
+            Flow.Data.Variable column.label, TDate
           else
-            throw new Error "Invalid column type #{column.type} found in frame #{frameKey}."
-      columnNames = (column.label for column in columns)
+            Flow.Data.Variable column.label, TObject
+
       Record = Flow.Data.Record columns
       rowCount = (head frame.columns).data.length
       rows = for i in [0 ... rowCount]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/floating_text_effect.coffee
+++ src/floating_text_effect.coffee
@@ -4,7 +4,7 @@

     # adds a FloatingTextEffect to the engine
     # at (50, 50). This effect will float upward
-    # at 90 pixels/sec and will fadeOut over 0
+    # at 90 pixels/sec and will fadeOut over 0.5 seconds
     engine.add 'FloatingTextEffect'
       x: 50
       y: 50


got failing acceptence test

got failing acceptence test

--- test/acceptance/coffee/ArchiveDocsTests.coffee
+++ test/acceptance/coffee/ArchiveDocsTests.coffee
@@ -150,3 +150,15 @@
 					for doc, i in non_deleted_docs
 						doc.lines.should.deep.equal @docs[i].lines
 					done()
+
+	describe "Unarchiving automatically", ->
+		it "should unarchive the docs ", (done) ->
+			DocstoreClient.archiveAllDoc @project_id, (error, res) =>
+				DocstoreClient.getDoc @project_id, @docs[0]._id, {}, (error, res, doc) =>
+					doc.lines.should.deep.equal @docs[0].lines
+					doc.version.should.equal @docs[0].version
+					doc.ranges.should.deep.equal @docs[0].ranges
+					done()
+
+
+


Added simple file ext comparison when Windows fails to detect MIME type

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -210,6 +210,13 @@
       else if file.type.indexOf('image/') is 0
         @state.files[file.name] = file
         gotFile = true
+      else if file.type? # When Windows fails to detect MIME type and returns an empty string for file.type
+        allowedFileExts = ['csv', 'tsv']
+        ext = file.name.split('.').pop()
+        if allowedFileExts.indexOf(ext) > -1
+          @_addManifest file
+          gotManifest = true
+

       if gotFile and not gotManifest
         @forceUpdate()


chore: fix ng dependencies

chore: fix ng dependencies

--- app/scripts/application.coffee
+++ app/scripts/application.coffee
@@ -1,9 +1,9 @@
 # The entry point for the application

 app = angular.module "myApp", [
+  "ngRoute"
   "ngAnimate"
-  "ngRoute"
-  "ngRoute"
+
   "myApp.templates"
   "myApp.alerts"
   "myApp.navigation"