Files
ember-panda/app/components/project/action-execution-offset-diagram.coffee
2017-12-21 11:51:55 +01:00

304 lines
14 KiB
CoffeeScript

#import Component from 'ember-c3/components/c3-chart'
import Ember from 'ember'
import groupBy from 'ember-group-by'
#<text text-anchor='end' transform="rotate(-90)" dx='-80' dy='0' style="opacity: 1;" x='-90' y='662'>asdf</text>
import WorkflowActionDefinitionOffsetNodes from 'ember-panda/utils/project/workflow-action-definition-offset-nodes'
{get} = Ember
GraphWrapper = Ember.Object.extend
records: []
absolute_value: 0
title: Ember.computed 'records.length', 'records.firstObject.title', ->
length = @get('records.length')
if length > 1
optional_text = @get('records').mapBy('title').join(' & ')
if optional_text.length <= 49
optional_text
else
actions_name = t('models.plural.workflow_action_definition')
"#{length} #{actions_name}"
else
@get('records.firstObject.title')
#GraphRecordWrapper = Ember.Object.extend
# title: Ember.computed 'record.title', 'value_shift', ->
# title = @get('record.title')
# title = " #{title} (#{value_shift}d)" if value_shift = @get('value_shift')
# title
#GraphOffsetWrapper = Ember.Object.extend {}
export default Ember.Component.extend
classNames: ['action-execution-offset-diagram-component']
graph_height: 320
bar_height: 20
x_axis_space: 60
left_space: 70
svg_width: 1200
window_min: null
window_max: null
range_start_default: null
range_start: null
range_end: null
range_bounds: {min: -7, max: 7}
range_formatter: (-> [{to: @days_offset_format.bind(@)}, {to: @days_offset_format.bind(@)}]).property()
reference_date_day_of_week: Ember.computed.oneWay 'project.reference_date_day_of_week'
choose_reference_date: true # choose a specific date to display as planning. Use X with migration date at specific day otherwise
chosen_absolute_date: null
batch_size_simulation_range: Ember.computed 'project.{batch_size_lower_value,batch_size_upper_value}',->
{min: @get('project.batch_size_lower_value'), max: @get('project.batch_size_upper_value')}
batch_size_simulation_value: 0
batch_size_formatter: (-> [to: (v) -> Math.round(v)]).property()
graph_title: Ember.computed 'window_min', 'window_max', 'range_start', 'range_end', 'chosen_absolute_date', ->
range_min = @days_offset_format @get('range_start')
range_max = @days_offset_format @get('range_end')
window_min = @days_offset_format @get('window_min')
window_max = @days_offset_format @get('window_max')
range_addition = ''
if range_min isnt window_min or range_max isnt window_max
range_addition = "(visible #{range_min} t/m #{range_max})"
base = "Showing #{t('models.workflow_action_definition')} offsets #{range_addition} having total window #{window_min} t/m #{window_max}"
base += " for #{t('attributes.project_instance.reference_date')} #{@days_offset_format(0)}" if @get('chosen_absolute_date')
base
days_offset_format: (v) ->
v = Math.round(v) # could be float from slider
if @get('chosen_absolute_date')
date = moment(@get('chosen_absolute_date'))
date.add(v, 'day') if v
date.format("D-M-YYYY")
else
return '' unless v
return '' unless v and Number.isFinite(v)
if v % 7 is 0
v = v / 7
if v > 0 then "X+#{v}w" else "X#{v}w"
else
if v > 0 then "X+#{v}" else "X#{v}"
actions:
range_changed: ([range_start, range_end]) ->
[range_start, range_end] = [Math.round(range_start), Math.round(range_end)]
@setProperties
range_start_default: [range_start, range_end]
range_start: range_start
range_end: range_end
@_setup_graph() # manual setup, do not use observable patter for range_start and range_end
batch_size_simulation_change: (value) ->
@setProperties
range_start: null
range_end: null
batch_size_simulation_value: Math.round(value)
hide_absolute_date_choice: ->
@set 'chosen_absolute_date', null # important! determinse wether the labels use X or dates
@set 'choose_reference_date', false
init: ->
@_super arguments...
#@set 'batch_size_simulation_value', 0 + @get('project.batch_size_lower_value') if @get('project.batch_size_lower_value')
@set 'batch_size_simulation_value', @get('project.batch_size_lower_value') if not @get('batch_size_simulation_value') and @get('project.batch_size_lower_value')
didReceiveAttrs: ->
console.log 'Reload workflow-action-offset-diagram call from didReceiveAttrs'
Ember.run.scheduleOnce 'afterRender', @, @_setup_graph
#_graph_observer: Ember.observer 'active_workflow_action_definitions.@each.title', 'active_workflow_action_definitions.@each.offset_value', 'range_start', 'range_end', 'batch_size_simulation_value', 'batch_size_simulation_range', 'reference_date_day_of_week', 'chosen_absolute_date', 'active_workflow_action_definitions.@each.id', ->
_graph_observer: Ember.observer 'project.update_revision', ->
console.log 'Reload workflow-action-offset-diagram call from observer'
console.log "update_revision: #{@get('project.update_revision')}, range_start: #{@get('range_start')}, range_end: #{@get('range_end')}"
Ember.run.debounce @, @_setup_graph, 150
_setup_graph: ->
console.log 'Reload workflow-actions-offset-diagram'
container_selector = '.action-execution-offset-diagram-container'
@$(container_selector).html('')
component = @
return unless @get('active_workflow_action_definitions.length')
batch_size_simulation_value = @get('batch_size_simulation_value') || 0
reference_date_day_of_week = @get('reference_date_day_of_week')
unless reference_date = @get('chosen_absolute_date')
reference_date = moment()
reference_date.day reference_date_day_of_week if reference_date_day_of_week # updates reference_date instance
result_nodes = WorkflowActionDefinitionOffsetNodes.create
workflow_action_definitions: @get('active_workflow_action_definitions')
reference_date: reference_date
batch_size: batch_size_simulation_value
resolved_record_nodes = result_nodes.get_resolved()
return unless get resolved_record_nodes, 'length'
wrappers = []
x_min = -1 # at least include zero
x_max = 0
batch_size_simulation_value = @get('batch_size_simulation_value')
resolved_record_nodes.forEach (record_wrapper) ->
if wrapper = wrappers.findBy 'absolute_value', record_wrapper.get('absolute_value')
wrapper.get('records').push record_wrapper
else
absolute_value = record_wrapper.get('absolute_value')
x_min = Math.min(x_min, absolute_value)
x_max = Math.max(x_max, absolute_value)
wrappers.push GraphWrapper.create
records: Ember.A([record_wrapper])
absolute_value: absolute_value
date: record_wrapper.get('date')
@set 'window_min', x_min
@set 'window_max', x_max
##data = @get('workflow_action_definition_groups')
##return unless data.length
#x_min = d3.min(data.mapBy('absolute_value'))
#x_min = -1 if x_min > 0 # add space for execution line
#x_max = d3.max(data.mapBy('absolute_value'))
#x_max = 0 if x_max < 0
#x_max += 1 # last ending can have duration 1 when empty
if @get('range_bounds.min') isnt x_min or @get('range_bounds.max') isnt x_max
@set 'range_bounds', min: x_min, max: x_max
@set 'range_start_default', [x_min, x_max] # bounds have changed, reset starts
else if not @get('range_start_default') # uninitialized
@set 'range_start_default', [x_min, x_max]
@set 'range_start', x_min unless Number.isFinite(@get('range_start'))
@set 'range_end', x_max unless Number.isFinite(@get('range_end'))
graph_min = @get('range_start')
graph_max = @get('range_end')
graph_range_width = @get('svg_width') - @get('left_space') - 22
x = d3.scale.linear().domain([graph_min, graph_max]).range([0, graph_range_width])
#yscale = d3.scale.linear().domain([0, @get('number_of_groups')]).range([0, @get('graph_height')])
#y = d3.scale.ordinal().rangeRoundBands([@get('graph_height'), 0], 0.1).domain(workflow_groups.mapBy('name').reverse())
#x = d3.scale.linear().domain([x_min, x_max]).range([0, 872])
y = d3.scale.linear().domain([0, 1]).range([0, @get('graph_height')])
x_ticks = wrappers.mapBy 'absolute_value'
#data.forEach (group) ->
# offset = Ember.get(group, 'value')
# x_ticks.push offset unless offset is 0 or x_ticks.indexOf(offset) > -1
x_ticks = x_ticks.filter( (v) -> v >= graph_min and v <= graph_max).sort(d3.ascending)
displayed_x_ticks = []
if x_ticks.length
displayed_x_ticks.push x_ticks[0]
latest_pixel_x_for_space = x(x_ticks[0])
x_ticks.forEach (v) ->
current_pixel_x = x(v)
if current_pixel_x - latest_pixel_x_for_space > 14
latest_pixel_x_for_space = current_pixel_x
displayed_x_ticks.push v
xAxis = d3.svg.axis()
.orient('bottom')
.scale(x)
.tickSize(1)
.tickValues(displayed_x_ticks)
.tickFormat component.days_offset_format.bind(component)
@$(container_selector).append '<svg></svg>'
svg_height = @get('x_axis_space') + @get('graph_height')
canvas = d3.select("#{container_selector} svg")
.attr 'width', '100%'
.attr 'height', svg_height
.attr 'viewBox', "0 0 #{@get('svg_width')} #{svg_height}"
lines = canvas.append('g')
.attr('class', 'offset-lines')
.attr('transform', "translate(#{@get('left_space')},0)")
.selectAll('line')
.data(x_ticks) # first element is y-axis
.enter()
.append('line')
.attr(
x1: (d) -> x(d)
y1: 0
x2: (d) -> x(d)
y2: y(1)
class: 'offset-line'
)
offset_text = (record_wrapper) ->
title_text = "#{record_wrapper.get('record.workflow_group.name')} -> #{record_wrapper.get('title')}"
offset_texts = []
record_wrapper.get('offset_wrappers').forEach (offset_wrapper) ->
given_offset = offset_wrapper.get('total_offset') || 0
day_text = if Math.abs(given_offset) > 1 then t('date.unit.plural.day') else t('date.unit.day')
offset_spec = if given_offset then "#{given_offset} #{day_text}" else 'direct'
offset_spec += " (#{value_shift}d)" if value_shift = offset_wrapper.get('value_shift')
if offset_base_action_title = offset_wrapper.get('record.base_workflow_action_definition.title')
offset_base_group_title = offset_wrapper.get('record.base_workflow_action_definition.workflow_group.name')
offset_relation_text = "#{offset_spec}: #{offset_base_group_title} -> #{offset_base_action_title}"
else
offset_relation_text = "#{offset_spec} from X"
offset_texts.push "Offset: #{offset_relation_text}"
"<button class='ui tiny icon button pull-right edit-workflow-action-definition' data-workflow-action-definition='#{record_wrapper.get('record_id')}'><i class='pencil icon'/></button>#{title_text}<br>#{offset_texts.join('<br>')}"
texts = canvas.append('g')
.attr('class', 'action-descriptions')
.attr('transform', "translate(#{@get('left_space')},0)")
.selectAll('text')
.data(wrappers.filter((d) -> (d.get('absolute_value') >= graph_min) and (d.get('absolute_value') <= graph_max)))
.enter()
.append('text')
.attr 'class', 'action-description-text'
.attr('transform', 'rotate(-90)')
.html (wrapper) ->
wrapper.get('title')
#.html (group) ->
# if group.items.length <= 2
# group.items.map( (record) ->
# "<tspan class='action-description-action-part' data-record-id='#{record.id}' tooltip='#{Ember.get(record, 'workflow_group.name')}'>#{Ember.get(record, 'title')}</tspan>"
# ).join(', ')
# else
# "<tspan>#{group.items.length} #{actions_name}</tspan>"
.attr
x: 10 - @get('graph_height')
y: (d) -> x(Ember.get(d, 'absolute_value'))
dy: -5
dx: -5
.on 'click', (wrapper) ->
target = $(@)
unless target.hasClass 'tooltipstered'
target.tooltipster
content: 'Loading...'
contentAsHTML: true
selfDestruction: false
trigger: 'click'
interactive: true
functionReady: ->
$('.tooltipster-base .edit-workflow-action-definition').on 'click', ->
return target.tooltipster('close') unless id = $(@).data('workflowActionDefinition')
return target.tooltipster('close') unless target_workflow_action_definition = component.store.peekRecord('panda/workflow-action-definition', id)
component.modal 'workflow-action-definition/edit-workflow-action-definition',
model: target_workflow_action_definition
target.tooltipster('close')
record_offset_texts = wrapper.records.sortBy('record.absolute_position').map(offset_text)
if wrapper.records.length > 1
target.tooltipster 'content', record_offset_texts.join('<hr>')
else
target.tooltipster 'content', record_offset_texts[0]
target.tooltipster 'open'
if graph_min <= 0 <= graph_max
base_line = canvas.append('line').attr
x1: x(0)
y1: 0
x2: x(0)
y2: 15 + @get('graph_height')
class: 'base-line'
transform: "translate(#{@get('left_space')},0)"
x_xis = canvas.append('g')
.attr('transform', "translate(#{@get('left_space')},#{@get('graph_height')})")
.attr('class','x-axis')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', "rotate(-40)")
canvas.selectAll('.action-description-action-part').on 'click', (d) ->
return unless workflow_action_definition_id = $(@).data('recordId')
return unless workflow_action_definition = component.store.peekRecord('panda/workflow-action-definition', workflow_action_definition_id)
component.modal 'workflow-action-definition/edit-workflow-action-definition',
model: workflow_action_definition