Public
Edited
Jan 7, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
json = FileAttachment("d3-lafe-example@14.json").json()
Insert cell
Insert cell
Insert cell
lafe = init(json)
Insert cell
function init(data){
let lafe = {}

//deep clone input data to keep orginal json object untouched
lafe.data = JSON.parse(JSON.stringify(data))

// set the dimensions and margins of the graph
lafe.margin = {top: 10, right: 10, bottom: 10, left: 10}
lafe.width = lafe.data.layout.width - lafe.margin.left - lafe.margin.right
lafe.height = lafe.data.layout.height - lafe.margin.top - lafe.margin.bottom

lafe.zoom = false //Initial zoom state of the graph

lafe.px = 1.0/lafe.width //Relative size of one pixel in the horizontal direction (x)
lafe.py = 1.0/lafe.height //Relative size of one pixel in the vertical direction (y)

//Graph options, managed by checkboxes or labels displayed directly within the graph
lafe.options = [
{ "code":"dShw",
"type":"checkbox",
"value":true,
"title":"Show intermediate demands",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"lclick",
"type":"label",
"title":"Left-click on a node to highlight genealogy",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"pGrp",
"type":"checkbox",
"value":true,
"title":"Group by product",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"dArg",
"type":"checkbox",
"value":true,
"title":"Auto-arrange demands",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"rclick",
"type":"label",
"title":"Right-click on a node to toggle zoom",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"pNrm",
"type":"checkbox",
"value":true,
"title":"Normalize by product",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0}
]
lafe.oMap = getMap(lafe.options)

//Keeping only consistent data and storing them
//into arrays (to keep original order) and maps (for convenient access)
lafe.demands = lafe.data.demands
.filter(d=>lafe.data.lot2demand.filter(l2d=>l2d.odemand==d.code).length>0)
lafe.dMap = getMap(lafe.demands)

lafe.resources = lafe.data.resources
.filter(r=>lafe.data.lots.filter(l=>l.resource==r.code).length>0)
lafe.rMap = getMap(lafe.resources)

lafe.stages = lafe.data.stages
.filter(s=>lafe.data.products.filter(p=>p.stage==s.code).length>0)
lafe.sMap = getMap(lafe.stages)

lafe.products = lafe.data.products
.filter(p=>lafe.sMap.has(p.stage))
.filter(p=>lafe.data.lots.filter(l=>l.product==p.code).length>0)
lafe.pMap = getMap(lafe.products)
lafe.lots = lafe.data.lots
.filter(l=>lafe.pMap.has(l.product)&&lafe.rMap.has(l.resource))
lafe.lMap = getMap(lafe.lots)
lafe.l2l = lafe.data.lot2lot
.filter(l2l=>lafe.lMap.has(l2l.ilot)&&lafe.lMap.has(l2l.olot))
lafe.l2d = lafe.data.lot2demand
.filter(l2d=>lafe.lMap.has(l2d.ilot)&&lafe.dMap.has(l2d.odemand))

//building relationships between objects and adding required properties
lafe.demands.forEach(d=>{
d.il2dMap = d3.map() //input l2d links
d.size = 0
})
lafe.stages.forEach(s=>{
s.bdepth=0; //backward depth of the stage
s.lMap = d3.map() //lots of the stage
s.pMap = d3.map() //products of the stage
s.pCount = //total number of products of the stage
s.lCount = //total number of lots of the stage
s.lSizeMax = 0.0 //maximum size of all lots of the stage
s.x = 0.0 //relative x coordinate of the stage
s.y = 0.0 //relative y coordinate of the stage
s.dx = 0.0 //relative width of the stage
s.dy = 0.0 //relative height of the stage
});
lafe.products.forEach(p=>{
p.stage = lafe.sMap.get(p.stage) //stage the product belongs to
p.lMap = d3.map() //lots of the product
p.lCount = //total number of lots of the product
p.lSizeMax = 0.0 //maximum size of all lots of the product
p.x = 0.0 //relative x coordinate of the product
p.y = 0.0 //relative y coordinate of the product
p.dx = 0.0 //relative width of the product
p.dy = 0.0 //relative height of the product
p.visible = true //visible state of the product
})
lafe.lots.forEach(l=>{
l.resource = lafe.rMap.get(l.resource); //resource the lot belongs to
l.product = lafe.pMap.get(l.product); //product the lot belongs to
l.date = new Date(l.date) //make date object from date string
l.il2lMap = d3.map(); //input l2l links
l.ol2lMap = d3.map(); //output l2l links
l.ol2dMap = d3.map(); //output l2d links
l.il2lSizes = d3.map() //total size of input l2l links per input product
l.ol2lSize = 0.0 //total size of output l2l links
l.ol2dSize = 0.0 //total size of output l2d links
l.x = 0.0 //relative x coordinate of the lot
l.y = 0.0 //relative y coordinate of the lot
l.dx = 0.0 //relative width of the lot
l.dy = 0.0 //relative height of the lot
l.selected = false //selected state of the lot
l.visible = true //visible state of the lot

//filling parent properties
l.product.lMap.set(l.code,l)
l.product.stage.lMap.set(l.code,l)
l.product.stage.pMap.set(l.product.code,l.product)
if(l.size>l.product.lSizeMax) l.product.lSizeMax = l.size
if(l.size>l.product.stage.lSizeMax) l.product.stage.lSizeMax = l.size

})
lafe.l2l.forEach(l2l=>{
l2l.ilot = lafe.lMap.get(l2l.ilot); //input (origin) lot of the lot 2 lot link
l2l.olot = lafe.lMap.get(l2l.olot); //output (destination) lot of the lot 2 lot link
l2l.ix = 0.0 //relative x coordinate of the input of the lot 2 lot link (upper-left corner of the band)
l2l.iy = 0.0 //relative y coordinate of the input of the lot 2 lot link (upper-left corner of the band)
l2l.idy = 0.0 //relative height of the input of the lot 2 lot link (left part of the band)
l2l.ox = 0.0 //relative x coordinate of the output of the lot 2 lot link (upper-right corner of the band)
l2l.oy = 0.0 //relative y coordinate of the output of the lot 2 lot link (upper-right corner of the band)
l2l.ody = 0.0 //relative height of the output of the lot 2 lot link (right part of the band)

//filling parent properties
l2l.ilot.ol2lMap.set(l2l.olot.code,l2l)
l2l.olot.il2lMap.set(l2l.ilot.code,l2l)
l2l.ilot.ol2lSize += l2l.size
addValue(l2l.olot.il2lSizes,l2l.ilot.product.code,l2l.size)

})
lafe.l2d.forEach(l2d=>{
l2d.ilot = lafe.lMap.get(l2d.ilot); //input (orgin) lot of the lot 2 demand link
l2d.odemand = lafe.dMap.get(l2d.odemand); //output (destinaion) demand of the lot 2 demand link
l2d.ix = 0.0 //relative x coordinate of the input of the lot 2 demand link (upper-left corner of the band)
l2d.iy = 0.0 //relative y coordinate of the input of the lot 2 demand link (upper-left corner of the band)
l2d.idy = 0.0 //relative height of the input of the lot 2 demand (left part of the band)
l2d.ox = 0.0 //relative x coordinate of the output of the lot 2 demand (upper-right corner of the band)
l2d.oy = 0.0 //relative y coordinate of the output of the lot 2 demand (upper-right corner of the band)
l2d.ody = 0.0 //relative height of the output of the lot 2 lot link (right part of the band)

//filling parent properties
l2d.odemand.size += l2d.size
l2d.ilot.ol2dMap.set(l2d.odemand.code,l2d)
l2d.odemand.il2dMap.set(l2d.ilot.code,l2d)
l2d.ilot.ol2dSize += l2d.size

})

//Recursively computing backward depth of stages,
//starting from demands and propagating backward
//considering also possible lots with no output links
lafe.demands.forEach(d=>bPropagate(d.il2dMap,0))
lafe.lots.filter(l=>(l.ol2lMap.size()+l.ol2dMap.size())==0).forEach(l=>bPropagate(l.il2lMap,1))
//Defining groups according to backward depths
//groups are used to group stages in such a way to
//minimize overlaping of links
//all lots inside a group will be positioned separately along the y axis
lafe.gCount = d3.set(lafe.stages.map(s=>s.bdepth)).size()
lafe.groups = [...Array(lafe.gCount).keys()].map(i=>{
let group = {
"code":"group-"+(i+1),
"index":i,
"bdepth":lafe.gCount-1-i,
"first":(i==0),
"last":(i==lafe.gCount-1),
"sCount":0, //number of stages in the group
"pCount":0, //number of products in the group
"lCount":0, //number of lots in the group
"tCount":0, //number of targets in the group
}
return group
})
lafe.gMap =getMap(lafe.groups)

//Assigning stages to groups and splitting demands into targets.
//One target is created for each couple group x demand for which l2d links exist
lafe.groups.forEach(g=>{
g.sMap = d3.map(lafe.stages.filter(s=>s.bdepth==g.bdepth),s=>s.code)
g.sCount = g.sMap.size()

g.tMap = d3.map() //targets of the group

g.sMap.each(s=>{
s.group = g
s.lMap.each(l=>{
l.ol2dMap.each(l2d=>{
//l2d is a lot to demand link with a lot part of the group
//a target is created for that demand within the group if not existing yet
let target = g.tMap.get(l2d.odemand.code)
if (target===undefined){
//create a new target (a target is a group-demand couple)
target = {
"code":g.code+"|"+l2d.odemand.code,
"sortkey":l2d.odemand.sortkey,
"group":g,
"demand":l2d.odemand,
"l2dMap":d3.map(), //map of all l2d links with this target as destination
"size":0, //total demand size of the target
"sizes":d3.map(), //demand size per product of the the target
"selected":false, //selected state of the target
"visible":true, //visible state of the target
"x":0.0, //relative x coordinate of the target
"y":0.0, //relative y coordinate of the target
"dx":0.0, //relative width of the target
"dy":0.0 //relative height of the target
}
g.tMap.set(l2d.odemand.code,target)
}
l2d.target = target
target.l2dMap.set(l2d.ilot.code,l2d)
target.size += l2d.size
addValue(target.sizes,l2d.ilot.product.code,l2d.size)

})
})
})

})

lafe.targets = lafe.groups.map(g=>sortedValues(g.tMap)).flat() //all targets of the graph

lafe.oCount = lafe.oMap.size() //total number of options
lafe.sCount = lafe.sMap.size() //total number of stages
lafe.pCount = lafe.pMap.size() //total number of products
lafe.lCount = lafe.lMap.size() //total number of lots
//elementary y padding space is the smallest y relative space used for vertical blank spacing between nodes //it corresponds to two pixels if possible, depending on the preferred height of the chart
//and the maxium number of nodes (bars) to align vertically
lafe.lPad = 1 //number of elementary y paddings used for lots
lafe.pPad = 2 //number of elementary y paddings used for products
lafe.sPad = 2 //number of elementary y paddings used for stages
lafe.tPad = 2 //number of elementary y paddings used for targets

return lafe
}
Insert cell
Insert cell
function computeSizes() {

//Reseting max sizes
lafe.groups.forEach(g=>{
g.tSizeMax = 0.0 //maximum size of all visible targets of the group
g.tSizeTot = 0.0 //total size of all visible targets of the group
})
lafe.stages.forEach(s=>{
s.lSizeMax = 0.0 //maximum size of all visible lots of the stage
})
lafe.products.forEach(p=>{
p.lSizeMax = 0.0 //maximum size of all visible lots of the product
})
//maximum value over all groups of the total number of elementary y paddings when grouping per stage
lafe.sPadMax = 0
//maximum value over all groups of the total number of elementary y paddings when grouping per product
lafe.pPadMax = 0

//Computing maximum and total sizes and based on visibility
lafe.targets.filter(t=>t.visible).forEach(t=>{
if(t.size>t.group.tSizeMax) t.group.tSizeMax = t.size
t.group.tSizeTot += t.size
})
lafe.lots.filter(l=>l.visible).forEach(l=>{
if(l.size>l.product.lSizeMax) l.product.lSizeMax = l.size
if(l.size>l.product.stage.lSizeMax) l.product.stage.lSizeMax = l.size
})
//counting visible lots, products, stages at the various levels,
//counting amount of elementary y padding spaces
//computing normalized sizes (normalization can be done over product or stage dimension),
lafe.groups.forEach(g=>{
g.sCount = 0 //number of visible stages of the group
g.pCount = 0 //number of visible products of the group
g.lCount = 0 //number of visible lots of the group
g.tCount = 0 //number of visible targets of the group

g.nsSize = 0 //Total normalized size of lots of the group when normalization is per stage
g.npSize = 0 //Total normalized size of lots of the group when normalization is per product

g.nsSizeL = 0 //Total normalized size of lots allocated to subsequent lots, normalization per stage
g.npSizeL = 0 //Total normalized size of lots allocated to subsequent lots, normalization per product
g.nsSizeD = 0 //Total normalized size of lots allocated to demands, normalization per stage
g.npSizeD = 0 //Total normalized size of lots allocated to demands, normalization per product

g.sPad = 0 //Total number of elementary y paddings of the group when lots are grouped per stage
g.pPad = 0 //Total number of elementary y paddings of the group when lots are grouped per product
g.sMap.each(s=>{
s.pCount = 0
s.nsSize = 0
s.npSize = 0
s.nsSizeL = 0
s.npSizeL = 0
s.nsSizeD = 0
s.npSizeD = 0
s.pMap.values().filter(p=>p.visible).forEach(p=>{
s.pCount += 1
p.first = g.first
p.last = g.last
p.lCount = 0
p.nsSize = 0
p.npSize = 0
p.nsSizeL = 0
p.npSizeL = 0
p.nsSizeD = 0
p.npSizeD = 0
p.lMap.values().filter(l=>l.visible).forEach(l=>{
p.lCount += 1
l.nsSize = s.lSizeMax>0?(l.size/s.lSizeMax):0.0 //normalizing lot sizes wrt to stage
l.npSize = p.lSizeMax>0?(l.size/p.lSizeMax):0.0 //normalizing lot sizes wrt to product
l.nsSizeL = s.lSizeMax>0?(l.ol2lSize/s.lSizeMax):0.0 //normalizing alloc. to lots wrt to stage
l.npSizeL = p.lSizeMax>0?(l.ol2lSize/p.lSizeMax):0.0 //normalizing alloc. to lots wrt to product
l.nsSizeD = s.lSizeMax>0?(l.ol2dSize/s.lSizeMax):0.0 //normalizing alloc. to demands wrt to stage
l.npSizeD = p.lSizeMax>0?(l.ol2dSize/p.lSizeMax):0.0 //normalizing alloc. to demands wrt to product
p.nsSize += l.nsSize
p.npSize += l.npSize
p.nsSizeL += l.nsSizeL
p.npSizeL += l.npSizeL
p.nsSizeD += l.nsSizeD
p.npSizeD += l.npSizeD
g.sPad += 2*lafe.lPad //counting upper and lower paddings around the lot (grouping per stage)
g.pPad += 2*lafe.lPad //counting upper and lower paddings around the lot (grouping per product)
},p)
s.lCount += p.lCount
s.nsSize += p.nsSize
s.npSize += p.npSize
s.nsSizeL += p.nsSizeL
s.npSizeL += p.npSizeL
s.nsSizeD += p.nsSizeD
s.npSizeD += p.npSizeD
g.pPad += 2*lafe.pPad //counting upper and lower paddings around the product (grouping per product)
//no paddings around product to account for when grouping per stage
},s)
g.lCount += s.lCount
g.pCount += s.pCount
g.sCount += 1
g.nsSize += s.nsSize
g.npSize += s.npSize
g.nsSizeL += s.nsSizeL
g.npSizeL += s.npSizeL
g.nsSizeD += s.nsSizeD
g.npSizeD += s.npSizeD
g.sPad += 2*lafe.sPad //counting upper and lower paddings around the stage (grouping per stage)
g.pPad += 2*lafe.sPad //counting upper and lower paddings around the stage (grouping per product)
},g)

g.tMap.values().filter(t=>t.visible).forEach(t=>{
t.last = g.last
t.nSize = g.tSizeMax>0?(t.size/g.tSizeMax):0.0
g.tCount += 1
})

if (lafe.sPadMax<g.sPad) lafe.sPadMax = g.sPad
if (lafe.pPadMax<g.pPad) lafe.pPadMax = g.pPad

})

//Computing space to be used for nodes and paddings
//(node are vertical bars used to represent lots and targets)
// number of vertical columns
//a column is used to veticaly align lots from a stage or targets from a group
lafe.cCount = lafe.sCount+1
//x space used for one node (10 pixels if not using more than 50% of the total x space, less otherwise)
lafe.nDX = Math.min(10*lafe.px,0.5/lafe.cCount)
//x space used between groups
lafe.gDX = (1.0-lafe.nDX)/lafe.gCount

//Number of columns used to arrange options
lafe.oCols = 3
//Number of rows used to arrange options
lafe.oRows = Math.ceil(lafe.oCount/lafe.oCols)
//y space used at the top part (for options) (10 pixels + 10 pixels per row)
lafe.topDY = (10+10*lafe.oRows)*lafe.py
//y space used at the bottom part (for graph)
lafe.botDY = 1.0-lafe.topDY

//y spacing used by one elementary padding
//2 pixels if not using more than 75% of bottom part y space, less otherwise
lafe.padY = Math.min(2*lafe.py,lafe.botDY*0.75/lafe.pPadMax)
lafe.lPadY = lafe.lPad*lafe.padY //relative padding y space used around lot
lafe.pPadY = lafe.pPad*lafe.padY //relative padding y space used around product
lafe.sPadY = lafe.sPad*lafe.padY //relative padding y space used around stage
lafe.tPadY = lafe.tPad*lafe.padY //relative padding y space used around targets

lafe.pPadX = 4*lafe.px //relative padding x space used around product
lafe.pDX = lafe.pPadX

}
Insert cell
Insert cell
function computePositions(){

let dShw = lafe.oMap.get("dShw").value //value of option "show intermediate demands"
let dArg = lafe.oMap.get("dArg").value //value of option "auto-arrange demands"
let pGrp = lafe.oMap.get("pGrp").value //value of option "group by product"
let pNrm = lafe.oMap.get("pNrm").value //value of option "normalize by product"

//Placing options
let dyy = lafe.topDY/lafe.oRows
lafe.options.forEach((o,i)=>{
let col = (i%lafe.oCols) //Column index of the option
let row = Math.floor(i/lafe.oCols) //Row index of the option

o.last = col==(lafe.oCols-1) //Option is place in the last column

o.x = col*(1.0/(lafe.oCols-1)) + (o.last?-lafe.pPadX:lafe.pPadX)
o.y = row*dyy
o.dx = 8*lafe.px // using 8x8 pixels to draw checkbox
o.dy = 8*lafe.py

})

//Computing x-y positions of nodes (lots and targets) and links
let x = 0.0
sortedValues(lafe.gMap).forEach(g=>{

//computing y space used by unitary normalized lot size for each group,
//depending on the type of grouping (per product or stage) and
//the type of normalization (per product or stage)
//y space used for the demand part is computed based on the ratio
//of the normalized size allocated to demands

//y space used for the intermediate demand part is computed based
//on the ratio of the normalized size allocated to demands
g.dDY = lafe.botDY *
(pNrm?
(((g.npSizeL+g.npSizeD)>0)?(g.npSizeD/(g.npSizeL+g.npSizeD)):0.0): //normalization per product
(((g.nsSizeL+g.nsSizeD)>0)?(g.nsSizeD/(g.nsSizeL+g.nsSizeD)):0.0) //normalization per stage
)

//Make sure that the part used for intermediate demands should at least occupy 2% of y space
if (g.tSizeTot>0){
g.dDY = Math.max(
g.dDY,
(0.02 + 2*(lafe.sPadY + (pGrp?lafe.pPadY:0)) + 2*g.tCount*lafe.tPadY)
)
}
//If intermediate demands are not displayed or if the group is the last one, reset related y space to 0
if (!dShw&&!g.last) g.dDY = 0

//y space used for empty space above lot part is computed based
//on the y space used for the demand part of the previous group
g.eDY = g.first?0.0:lafe.groups[g.index-1].dDY

//y space used for the lot part
g.lDY = lafe.botDY - g.eDY

//y space used for a lot node of unitary size
g.nDY = (g.lDY-(pGrp?g.pPad:g.sPad)*lafe.padY)/(pNrm?g.npSize:g.nsSize)

g.x = x
g.y = lafe.topDY

//Computing x-y positions of all lots, products, stages within the group
let y = g.y + g.eDY

//All stages within the group are positioned in the first 20% x space
//g.sDX is the x space between each stage of the group
g.sDX = g.sCount>1?((0.2*lafe.gDX)/(g.sCount-1)):0.0

let xx = x

sortedValues(g.sMap).forEach(s=>{
s.x = xx
s.dx = lafe.pDX

y += lafe.sPadY //Adding top padding space for the stage

s.y = y

if(pGrp){

//grouping per product
sortedValues(s.pMap).forEach(p=>{

p.x = s.x
p.dx = lafe.pDX

y += (p.visible?lafe.pPadY:0) //Adding top padding space for product if visible

p.y = y

//Sorting lots of the product according to date
p.lMap.values().sort((l1,l2)=>l1.date.getTime()-l2.date.getTime()).forEach(l=>{

y += (l.visible?lafe.lPadY:0) //Adding top padding space for lot if visible

l.x = p.x + lafe.pPadX //Adding left padding space between product (bracket) and lot
l.dx = lafe.nDX
l.y = y

//relative height of the lot is its normalized size (depending on the normalization mode)
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
l.dy = (l.visible?(g.nDY*(pNrm?l.npSize:l.nsSize)):0)

y += l.dy //move current y position by the lot height
y += (l.visible?lafe.lPadY:0) //add bottom padding space for lot if visible

},p)

p.dy = y - p.y //total height of the product

y += (p.visible?lafe.pPadY:0) //adding bottom padding space for product if visible

},s)

} else {

//grouping per stage

//Sorting lots of the stage according to date
s.lMap.values().sort((l1,l2)=>l1.date.getTime()-l2.date.getTime()).forEach(l=>{

y += (l.visible?lafe.lPadY:0) //adding top padding for the lot if visible

l.x = s.x + lafe.pPadX //Adding left padding space between product (bracket) and lot
l.dx = lafe.nDX

l.y = y
//relative height of the lot is its normalized size (depending on the normalization mode)
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
l.dy = (l.visible?(g.nDY*(pNrm?l.npSize:l.nsSize)):0)

y += l.dy //move current y position by the lot height
y += (l.visible?lafe.lPadY:0) //add bottom padding space for lot if visible

},s)

}

s.dy = y - s.y //total height of the stage

y += lafe.sPadY //Adding bottom padding space for the stage
xx += g.sDX //Moving xx coordinate by the space between stages within the group

},g)

x += lafe.gDX //Moving x coordinate by the space between groups

//at this level y should be equal to 1
//console.log("y = " + y)

//Computing y weight of all targets
//y weight of a target is the gravity center in the vertical direction
//of the y position of the inputs of all links allocated to the target.
//When auto-arranging demands is used, demands are ordered according to
//this weight in order to minimize crossing of lot to demand flows
g.tMap.each(t =>{
t.yweight = 0.0
t.l2dMap.each(l2d=>{
t.yweight += l2d.size*l2d.ilot.y
},t)
t.yweight = t.yweight/t.size
})

//Computing x-y positions of all targets
y = g.y + lafe.sPadY + (pGrp?lafe.pPadY:0) //Adding top padding of stage and product if visible

//y space used for a target node of unitary size
dyy = (g.tSizeTot>0&&g.dDY>0)?
(g.dDY - 2*(lafe.sPadY + (pGrp?lafe.pPadY:0)) - 2*g.tCount*lafe.tPadY)/g.tSizeTot
:0.0
//Sorting targets of the group according to demand original order or
//according to the weight (if auto-arrange is active)
g.tMap.values().sort(
(t1,t2)=>dArg?(t1.yweight-t2.yweight):(t1.demand.sortkey-t2.demand.sortkey)
).forEach(t => {
let show = t.visible&&(dShw||t.last)

y += (show?lafe.tPadY:0) //Adding top padding for target if visible
t.x = x + lafe.pPadX
t.y = y
t.dx = lafe.nDX //Using same relative width for targets and lots

//relative height of the target is its size multiplied by space of unitary size
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
t.dy = (show?(dyy*t.size):0)

y += t.dy //move current y position by the target height
y += (show?lafe.tPadY:0) //add bottom padding space for the target if visible

})

})
//Computing x-y positions of all links
//(ix,iy) are the coordinates of the upper-left corner of the band (input of the band)
//(ox,oy) are the coordinates of the upper-right corner of the band (output of the band)
//idy is the height of the input of the band
//ody is the height of the output of the band
lafe.gMap.each(g=>{
g.sMap.each(s=>{
s.pMap.each(p=>{
p.lMap.each(l=>{
let y = l.y
//sorting output lot to demand links wrt link output target y position
l.ol2dMap.values().sort((ol2d1,ol2d2)=>ol2d1.target.y-ol2d2.target.y).forEach(ol2d=>{
ol2d.ix = l.x + l.dx
ol2d.iy = y
//relative height of the link input is the fraction of the input lot height
//corresponding to the ratio of allocation of the lot to the demand
ol2d.idy = ((l.size>0)?ol2d.size/l.size:0.0)*l.dy
y += ol2d.idy //moving y position by the link input height
},l)
//sorting output lot to lot links wrt y positions of link output lot
l.ol2lMap.values().sort((ol2l1,ol2l2)=>(ol2l1.olot.y-ol2l2.olot.y)).forEach(ol2l=>{
ol2l.ix = l.x + l.dx
ol2l.iy = y
//relative height of the link input is the fraction of the input lot height
//corresponding to the ratio of allocation of the input lot to the output lot
ol2l.idy = ((l.size>0)?ol2l.size/l.size:0.0)*l.dy
y += ol2l.idy //moving y position by the link input height
},l)
//for each input product, sorting related input lot to lot links
//wrt to y positions of the link input lot
//y coordinate is reset to the top of the lot at each product to cause overlapping of links
//when multiple products are used as input of the lot
l.il2lSizes.keys().forEach(code=>{
y = l.y
//for one product, input links are stacked
//isolating links related to the product and
//sorting them according to the y position of the input lot
l.il2lMap.values().filter(il2l=>il2l.ilot.product.code==code).sort(
(il2l1,il2l2)=>il2l1.ilot.y - il2l2.ilot.y
).forEach(il2l=>{
il2l.ox = l.x
il2l.oy = y
//relative height of the link output is the fraction of the output lot height
//corrresponding to the ratio of contribution of the link to the total contribution
//of all links supplying the same product
il2l.ody = ((l.il2lSizes.get(code)>0)?il2l.size/l.il2lSizes.get(code):0.0)*l.dy
y += il2l.ody //moving y position by the link output height
},p)
},l)

},p)
},s)
},g)

g.tMap.each(t=>{
x = t.x
let y = t.y
//sorting input lot to demand links wrt link input lot y position
t.l2dMap.values().sort((l2d1,l2d2)=>l2d1.iy-l2d2.iy).forEach(l2d=>{
l2d.ox = x
l2d.oy = y
//relative height of the link output is the fraction of the output target height
//corresponding to the ratio of allocation of the lot to the demand
l2d.ody = (t.size>0?(l2d.size/t.size):0.0)*t.dy
y += l2d.ody
})
},g)

})

}
Insert cell
Insert cell
function setTitles(){

//Setting titles for lots
lafe.lots.forEach(l=>{
l.title =
l.code + "\n" + "\n" +
"stage: " + l.product.stage.code + "\n" +
"product: " + l.product.code + "\n" +
"resource: " + l.resource.code + "\n" + "\n" +
"date: " + formatDate(l.date) + "\n" + "\n" +
"size: " + formatSize(l.size,l.product.unit) + "\n" +
" → allocated to demands: " + formatSize(l.ol2dSize) + "\n" +
" → allocated to lots: " + formatSize(l.ol2lSize) + "\n" +
" → unallocated: " + formatSize(l.size-l.ol2lSize-l.ol2dSize) + "\n"

})
//Setting titles for targets
lafe.targets.forEach(t=>{
t.title =
t.demand.code + "\n" + "\n" +
"size: " + "\n" +
t.sizes.entries().map(e=>" → "+e.key+": "+formatSize(e.value,lafe.pMap.get(e.key).unit)+"\n").join('')
},lafe)
//Setting titles for lot to lot links
lafe.l2l.forEach(l2l=>{
l2l.title =
l2l.ilot.code + " → " + l2l.olot.code + "\n" + "\n" +
"size: " + formatSize(l2l.size,l2l.ilot.product.unit) + "\n" +
" → consuming " + formatRatio(l2l.ilot.size==0?0.0:l2l.size/l2l.ilot.size) + " of " + l2l.ilot.code + "\n" +
" → providing " + formatRatio(l2l.olot.il2lSizes.get(l2l.ilot.product.code)==0?0.0:l2l.size/l2l.olot.il2lSizes.get(l2l.ilot.product.code)) + " of " + l2l.olot.code + " need " + " \n"
},lafe)

//Setting titles for lot to demand links
lafe.l2d.forEach(l2d=>{
l2d.title =
l2d.ilot.code + " → " + l2d.odemand.code + "\n" + "\n" +
"size: " + "\n" +
formatSize(l2d.size,l2d.ilot.product.unit) + " = " + formatRatio(l2d.ilot.size==0?0.0:l2d.size/l2d.ilot.size) + "\n"
})

}
Insert cell
Insert cell
function update(){

//Update all positions
computePositions()
let dShw = lafe.oMap.get("dShw").value //value of option "show intermediate demands"
let dArg = lafe.oMap.get("dArg").value //value of option "auto-arrange demands"
let pGrp = lafe.oMap.get("pGrp").value //value of option "group by product"
let pNrm = lafe.oMap.get("pNrm").value //value of option "normalize by product"

var duration = 2000 //duration of the transitions

//Animate (un)activation of option checkboxes
lafe.oElements.select("path").transition().duration(duration)
.attr("fill",o=>o.value?"#aaa":"#fff")

//Hide/show stuff
lafe.pBrackets.transition().delay(p=>pShow(p)?(duration/2):0).duration(p=>pShow(p)?(duration/2):0)
.attr("opacity",p=>pShow(p)?1.0:0.0)

lafe.lNodes.transition().delay(l=>lShow(l)?0:duration)
.attr("visibility",l=>lShow(l)?"visible":"hidden")

lafe.dNodes.transition().delay(t=>tShow(t)?0:duration)
.attr("visibility",t=>tShow(t)?"visible":"hidden")

lafe.l2lLinks.transition().delay(l2l=>l2lShow(l2l)?0:duration)
.attr("visibility",l2l=>l2lShow(l2l)?"visible":"hidden")

lafe.l2dLinks.transition().delay(l2d=>l2dShow(l2d)?0:duration)
.attr("visibility",l2d=>l2dShow(l2d)?"visible":"hidden")

//Update positions
lafe.pBrackets.select("path").transition().duration(duration)
.attr("d",p=>pBracketD(p,lafe.width,lafe.height))

lafe.pBrackets.select("text").transition().duration(duration)
.attr("x",p=>pBracketTextX(p,lafe.width))
.attr("y",p=>pBracketTextY(p,lafe.height))

lafe.l2lLinks.select("path").transition().duration(duration)
.attr("d",l2l=>l2lLinkD(l2l,lafe.width,lafe.height))

lafe.l2dLinks.select("path").transition().duration(duration)
.attr("d",l2d=>l2dLinkD(l2d,lafe.width,lafe.height))

lafe.lNodes.select("path.top").transition().duration(duration)
.attr("d",l=>lNodeTopD(l,lafe.width,lafe.height))

lafe.lNodes.select("path.bottom").transition().duration(duration)
.attr("d",l=>lNodeBottomD(l,lafe.width,lafe.height))

lafe.lNodes.select("text").transition().duration(duration)
.attr("x",l=>lNodeTextX(l,lafe.width))
.attr("y",l=>lNodeTextY(l,lafe.height))

lafe.dNodes.select("path").transition().duration(duration)
.attr("d",t=>dNodeD(t,lafe.width,lafe.height))
lafe.dNodes.select("text").transition().duration(duration)
.attr("x",t=>dNodeTextX(t,lafe.width))
.attr("y",t=>dNodeTextY(t,lafe.height))


}
Insert cell
Insert cell
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sept","Oct","Nov","Dec"];
Insert cell
format0 = d3.format(",.0f")

Insert cell
format2 = d3.format(",.2f")
Insert cell
function formatDate(date){

var year = date.getFullYear();
var month = months[date.getMonth()];
var day = date.getDate();

return day + '-' + month + '-' + year;

}
Insert cell
function formatSize(size,unit){
return format0(size) + (unit===undefined?"":(" [" + unit +"]"))
}
Insert cell
function formatRatio(ratio){
return format2(100*ratio) + "%"
}
Insert cell
//Assuming an array of objects containing each a code property
//Adds sortkey property to each object and return a map of the objects using code as key
function getMap(array){
array.forEach((o,i)=>o.sortkey=i)
return d3.map(array,o=>o.code)
}
Insert cell
//Assuming a map of objects containing each a sortkey property
//Returns an array of objects sorted according to the sortkey
function sortedValues(map){
return map.values().sort((o1,o2)=>o1.sortkey-o2.sortkey)
}
Insert cell
//Assuming a map with numeric values
//Increments value for key, if key not found create it with 0.0
function addValue(map,key,value){
map.set(key,(map.has(key)?map.get(key):0.0)+value)
}
Insert cell
//Builds the "d" attribute of a path representing a bracket [
//(x,y) are the relative coordinate of the upper-left corner of the bracket
//(dx,dy) are the relative width and height of the bracket
//width and height are the absolute dimensions of the container of the bracket
function bracketD(x,y,dx,dy,width,height){

//absolute coordinates of the bracket [
let x0 = Math.round(width*x)
let y0 = Math.round(height*y)
let x1 = Math.round(width*(x+dx))
let y1 = Math.round(height*y)
let x2 = Math.round(width*(x+dx))
let y2 = Math.round(height*(y+dy))
let x3 = Math.round(width*x)
let y3 = Math.round(height*(y+dy))

let d = ""
d += "M " + x1 + " " + y1 //Move to (x1,y1)
d += " L " + x0 + " " + y0 //Line to (x0,y0)
d += " L " + x3 + " " + y3 //Line to (x3,y3)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
return d

}
Insert cell
//Builds the "d" attribute of a path representing a rectangle
//(x,y) are the relative coordinate of the upper-left corner of the rectangle
//(dx,dy) are the relative width and height of the rectangle
//width and height are the absolute dimensions of the container of the rectangle
function rectangleD(x,y,dx,dy,width,height){

//absolute coordinates of the rectangle
let x0 = Math.round(width*x)
let y0 = Math.round(height*y)
let x1 = Math.round(width*(x+dx))
let y1 = Math.round(height*y)
let x2 = Math.round(width*(x+dx))
let y2 = Math.round(height*(y+dy))
let x3 = Math.round(width*x)
let y3 = Math.round(height*(y+dy))

let d = ""
d += "M " + x0 + " " + y0 //Move to (x0,y0)
d += " L " + x1 + " " + y1 //Line to (x1,y1)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
d += " L " + x3 + " " + y3 //Line to (x3,y3)
d += " L " + x0 + " " + y0 //Line to (x0,y0)
return d

}
Insert cell
//Builds the "d" attribute of a path representing a band with vertical left and right bondaries
//and curvy top and bottom bondaries
//(ix,iy) are the relative coordinate of the upper-left corner of the band (input)
//(ox,oy) are the relative coordinate of the upper-right corner of the band (output)
//(idy,ody) are the relative heights of the left (input) and right (output) boundaries
//width and height are the absolute dimensions of the container of the band
function bandD(ix,iy,idy,ox,oy,ody,width,height){

var curvature = 0.5
//absolute coordinates of the band
let x0 = Math.round(width*ix)
let y0 = Math.round(height*iy)
let x1 = Math.round(width*ox)
let y1 = Math.round(height*oy)
let x2 = Math.round(width*ox)
let y2 = Math.round(height*(oy + ody))
let x3 = Math.round(width*ix)
let y3 = Math.round(height*(iy + idy))
let dx = Math.round((x1-x0)*curvature)
let d = ""

d += "M " + x0 + " " + y0 //Move to (x0,y0)
d += " C " + (x0+dx) + " " + y0 + " " + (x1-dx) + " " + y1 + " "+ x1 + " " + y1 //Cubic curve to (x1,y1)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
d += " C " + (x2-dx) + " " + y2 + " " + (x3+dx) + " " + y3 + " "+ x3 + " " + y3 //Cubic curve to (x3,y3)
d += " L " + x0 + " " + y0 //Line to (x0,y0)

return d

}
Insert cell
Insert cell
//propagate backward depth
function bPropagate(linkMap,depth){
linkMap.each(link =>{
if (link.ilot.product.stage.bdepth<depth) link.ilot.product.stage.bdepth = depth
bPropagate(link.ilot.il2lMap,depth+1)
})
}
Insert cell
function oCheckboxD(o,width,height){
let x = o.x
let y = o.y
let dx = o.dx
let dy = o.dy

return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function oCheckboxX(o,width){
return Math.round(width*(o.x + (o.last?-0.5*o.dx:1.5*o.dx)))
}
Insert cell
function oCheckboxY(o,height){
return Math.round(height*o.y)
}
Insert cell
function pBracketD(p,width,height){
let x = p.x
let y = p.y
let dx = p.dx
let dy = p.dy
return bracketD(x,y,dx,dy,width,height)
}
Insert cell
function pBracketTextX(p,width){
return Math.round(width*(p.first?(p.x+2*p.dx+lafe.nDX):(p.x-p.dx)))
}
Insert cell
function pBracketTextY(p,height){
return Math.round(height*p.y)
}
Insert cell
function lNodeTopD(l,width,height){
let ry = (l.size>0)?((l.ol2lSize+l.ol2dSize)/l.size):0.0
let x = l.x
let y = l.y
let dx = l.dx
let dy = l.dy*ry
return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function lNodeBottomD(l,width,height){
let ry = (l.size>0)?((l.ol2lSize+l.ol2dSize)/l.size):0.0
let x = l.x
let y = l.y+l.dy*ry
let dx = l.dx
let dy = l.dy*(1-ry)
return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function lNodeTextX(l,width){
return Math.round(width*(l.x+1.2*l.dx))
}
Insert cell
function lNodeTextY(l,height){
return Math.round(height*(l.y+0.5*l.dy))
}
Insert cell
function dNodeD(t,width,height){
let x = t.x
let y = t.y
let dx = t.dx
let dy = t.dy

return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function dNodeTextX(t,width){
return Math.round(width*(t.x+(t.last?-0.2:1.2)*t.dx))
}
Insert cell
function dNodeTextY(t,height){
return Math.round(height*(t.y+0.5*t.dy))
}
Insert cell
function l2lLinkD(l2l,width,height){
let ix = l2l.ix
let iy = l2l.iy
let idy = l2l.idy
let ox = l2l.ox
let oy = l2l.oy
let ody = l2l.ody

return bandD(ix,iy,idy,ox,oy,ody,width,height)
}
Insert cell
function l2dLinkD(l2d,width,height){
let ix = l2d.ix
let iy = l2d.iy
let idy = l2d.idy
let ox = l2d.ox
let oy = l2d.oy
let ody = l2d.ody

return bandD(ix,iy,idy,ox,oy,ody,width,height)
}
Insert cell
function l2lShowFwd(l2l){

l2l.olot.visible = true
l2l.olot.product.visible = true
l2l.olot.ol2lMap.each(ol2l=>{
l2lShowFwd(ol2l)
})
l2l.olot.ol2dMap.each(ol2d=>{
l2dShowFwd(ol2d)
})


}
Insert cell
function l2lShowBwd(l2l){

l2l.ilot.visible = true
l2l.ilot.product.visible = true
l2l.ilot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
}
Insert cell
function l2dShowFwd(l2d){

l2d.target.visible = true

}
Insert cell
function l2dShowBwd(l2d){

l2d.ilot.visible = true
l2d.ilot.product.visible = true
l2d.ilot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
}
Insert cell
function l2lRevealFwd(l2l){

lafe.l2lLinks.filter(x=>x==l2l).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2l.olot).classed("reveal",true)

l2l.olot.ol2lMap.each(ol2l=>{
l2lRevealFwd(ol2l)
})
l2l.olot.ol2dMap.each(ol2d=>{
l2dRevealFwd(ol2d)
})


}
Insert cell
function l2lRevealBwd(l2l){

lafe.l2lLinks.filter(x=>x==l2l).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2l.ilot).classed("reveal",true)

l2l.ilot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
}
Insert cell
function l2dRevealFwd(l2d){

lafe.l2dLinks.filter(x=>x==l2d).classed("reveal",true)
lafe.dNodes.filter(t=>t==l2d.target).classed("reveal",true)

}
Insert cell
function l2dRevealBwd(l2d){

lafe.l2dLinks.filter(x=>x==l2d).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2d.ilot).classed("reveal",true)

l2d.ilot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
}
Insert cell
function resetVisible(){

lafe.products.forEach(p=>p.visible=!lafe.zoom)
lafe.lots.forEach(l=>l.visible=!lafe.zoom)
lafe.targets.forEach(t=>t.visible=!lafe.zoom)

}
Insert cell
function resetSelection(){

//Clear selected flags
lafe.lots.forEach(l=>l.selected=false)
lafe.targets.forEach(t=>t.selected=false)

//Clear reveal and select classes
lafe.lNodes.classed("select",false)
lafe.lNodes.classed("reveal",false)
lafe.dNodes.classed("select",false)
lafe.dNodes.classed("reveal",false)
lafe.l2lLinks.classed("reveal",false)
lafe.l2dLinks.classed("reveal",false)
}
Insert cell
function lClickL(lot){

let selected = lot.selected
resetSelection()
if(!selected){
lot.selected = true
lafe.lNodes.filter(l=>l==lot).classed("select",true)
lot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
lot.ol2lMap.each(ol2l=>{
l2lRevealFwd(ol2l)
})
lot.ol2dMap.each(ol2d=>{
l2dRevealFwd(ol2d)
})
}

}
Insert cell
function lClickR(lot){

lafe.zoom = !lafe.zoom
resetSelection()
resetVisible()

if(lafe.zoom){

lot.visible = true
lot.product.visible = true
lot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
lot.ol2lMap.each(ol2l=>{
l2lShowFwd(ol2l)
})
lot.ol2dMap.each(ol2d=>{
l2dShowFwd(ol2d)
})

}

computeSizes()
update()

}
Insert cell
function tClickL(target){

let selected = target.selected
resetSelection()
if(!selected){
target.selected = true
lafe.dNodes.filter(t=>t==target).classed("select",true)
target.l2dMap.each(il2d=>{
l2dRevealBwd(il2d)
})
}

}
Insert cell
function tClickR(target){

lafe.zoom = !lafe.zoom
resetSelection()
resetVisible()

if(lafe.zoom){

target.visible = true
target.l2dMap.each(il2d=>{
l2dShowBwd(il2d)
})
}

computeSizes()
update()

}
Insert cell
function pShow(p){
return lafe.oMap.get("pGrp").value&&p.visible
}
Insert cell
function lShow(l){
return l.visible
}
Insert cell
function tShow(t){
return (lafe.oMap.get("dShw").value||t.last)&&t.visible
}
Insert cell
function l2lShow(l2l){
return l2l.ilot.visible&&l2l.olot.visible
}
Insert cell
function l2dShow(l2d){
return (lafe.oMap.get("dShw").value||l2d.target.last)&&l2d.target.visible&&l2d.ilot.visible
}
Insert cell
function fillSVG(svg,scale){

let maxWidth = (lafe.width + lafe.margin.left + lafe.margin.right)
let maxHeight = (lafe.height + lafe.margin.top + lafe.margin.bottom)

svg.attr("class", "lafe")

if (scale)
svg.attr("width","100%")
.attr("viewBox", "0 0 " + maxWidth + " " + maxHeight)
else
svg.attr("width",maxWidth)
.attr("height",maxHeight)

let g = svg
.append("g")
.attr("transform",
"translate(" + lafe.margin.left + "," + lafe.margin.top + ")");

// add the option elements
lafe.oElements = g.append("g").selectAll(".oElement")
.data(lafe.options)
.enter()
.append("g")
.attr("class", "oElement")

// add the option element checkbox shapes
lafe.oElements.filter(o=>o.type=="checkbox").append("path")
.attr("fill",o=>o.value?"#aaa":"#fff")
.attr("d",o=>oCheckboxD(o,lafe.width,lafe.height))
.on("click",function(o){
o.value = !o.value
update()
})

// add the option element texts
lafe.oElements.append("text")
.attr("x",o=>oCheckboxX(o,lafe.width))
.attr("y",o=>oCheckboxY(o,lafe.height))
.attr("dy",".7em")
.attr("text-anchor",o=>o.last?"end":"start")
.text(o=>o.title)

// add the product brackets
lafe.pBrackets = g.append("g").selectAll(".pBracket")
.data(lafe.products)
.enter()
.append("g")
.attr("class", "pBracket")

// add the product bracket shapes
lafe.pBrackets.append("path")
.attr("d",p=>pBracketD(p,lafe.width,lafe.height))
.append("title")
.text(p=>p.title);

// add the product bracket labels
lafe.pBrackets.append("text")
.attr("x",p=>pBracketTextX(p,lafe.width))
.attr("y",p=>pBracketTextY(p,lafe.height))
.attr("dy",".7em")
.attr("text-anchor",p=>(p.first?"start":"end"))
.text(p=>p.code)

// add the l2l links
lafe.l2lLinks = g.append("g").selectAll(".l2lLink")
.data(lafe.l2l)
.enter()
.append("g")
.attr("class", "l2lLink")

// add the l2l link shapes
lafe.l2lLinks.append("path")
.attr("d",l2l=>l2lLinkD(l2l,lafe.width,lafe.height))
.append("title")
.text(l2l=>l2l.title);

//add the l2d links
lafe.l2dLinks = g.append("g").selectAll(".l2dLink")
.data(lafe.l2d)
.enter()
.append("g")
.attr("class", "l2dLink")

// add the l2d link shapes
lafe.l2dLinks.append("path")
.attr("d",l2d=>l2dLinkD(l2d,lafe.width,lafe.height))
.append("title")
.text(l2d=>l2d.title);

// add the lot nodes
lafe.lNodes = g.append("g").selectAll(".lNode")
.data(lafe.lots)
.enter()
.append("g")
.attr("class", "lNode")
.on("click",function(l){
lClickL(l)
})
.on("contextmenu",function(l){
d3.event.preventDefault();
lClickR(l)
})

// add the lot node top shapes
lafe.lNodes.append("path")
.attr("d",l=>lNodeTopD(l,lafe.width,lafe.height))
.attr("class", "top")

//add the lot node bottom shapes
lafe.lNodes.append("path")
.attr("d",l=>lNodeBottomD(l,lafe.width,lafe.height))
.attr("class", "bottom")

//add the lot node titles
lafe.lNodes.append("title")
.text(l=>l.title);

// add the lot node labels
lafe.lNodes.append("text")
.attr("x",l=>lNodeTextX(l,lafe.width) )
.attr("y",l=>lNodeTextY(l,lafe.height))
.attr("dy",".35em")
.attr("text-anchor","start")
.text(l=>l.code)

// add the demand (target) nodes
lafe.dNodes = g.append("g").selectAll(".dNode")
.data(lafe.targets)
.enter()
.append("g")
.attr("class", "dNode")
.on("click",function(t){
tClickL(t)
})
.on("contextmenu",function(t){
d3.event.preventDefault();
tClickR(t)
})

// add the demand (target) node shapes
lafe.dNodes.append("path")
.attr("d",t=>dNodeD(t,lafe.width,lafe.height))
.append("title")
.text(t=>t.title);
//add the demand (target) node labels
lafe.dNodes.append("text")
.attr("x",t=>dNodeTextX(t,lafe.width))
.attr("y",t=>dNodeTextY(t,lafe.height))
.attr("dy",t=>".35em")
.attr("text-anchor",t=>t.last?"end":"start")
.text(t=>t.demand.code)

}
Insert cell
Insert cell
getSVG = function(){
let svg = d3.create("svg")

computeSizes()
computePositions()
setTitles()
fillSVG(svg,true)

return svg
}
Insert cell
Insert cell
html`
<style>
:root {
--oelement-color: black;
--label-color: grey;
--p-color-h: 077;
--p-color-s: 50%;
--p-color-l: 50%;
--l-color-h: 190;
--l-color-s: 80%;
--l-color-l: 40%;
--d-color-h: 033;
--d-color-s: 76%;
--d-color-l: 64%;
--r-color-h: 300;
--r-color-s: 80%;
--r-color-l: 80%;
}

.lafe {
font-size: 10px;
text-shadow: 0 1px 0 #fff;
}

.oElement {
--color: var(--oelement-color);
}

.oElement:hover {
font-weight: bold;
}

.oElement path {
stroke: var(--color);
}

.oElement text {
fill: var(--label-color);
pointer-events: none;
}

.pBracket {
--color-h: var(--p-color-h);
--color-s: var(--p-color-s);
--color-l: var(--p-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
}

.pBracket path{
stroke: var(--color);
fill: none;
}

.pBracket text {
fill: var(--color);
pointer-events: none;
}

.lNode {
--color-h: var(--l-color-h);
--color-s: var(--l-color-s);
--color-l: var(--l-color-l);
--darken: 1.0;
--color: hsl(var(--color-h),var(--color-s),calc(var(--color-l)*var(--darken)));
}

.lNode.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
}

.lNode:hover {
font-weight: bold;
--darken: 0.7;
}

.lNode.select{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l)*0.5;
}

.lNode .top{
stroke: var(--color);
fill: var(--color)
}

.lNode .bottom{
stroke: var(--color);
fill: white;
}

.lNode text {
fill: var(--label-color);
pointer-events: none;
}


.dNode {
--color-h: var(--d-color-h);
--color-s: var(--d-color-s);
--color-l: var(--d-color-l);
--darken: 1.0;
--color: hsl(var(--color-h),var(--color-s),calc(var(--color-l)*var(--darken)));
}

.dNode.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
}

.dNode:hover {
font-weight: bold;
--darken: 0.7;
}

.dNode.select{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l)*0.5;
}

.dNode path{
stroke: var(--color);
fill: var(--color);
}

.dNode text {
fill: var(--label-color);
}

.l2lLink {
--color-h: var(--l-color-h);
--color-s: var(--l-color-s);
--color-l: var(--l-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
--opacity: .1;

fill: var(--color);
fill-opacity: var(--opacity);
stroke: none;
}

.l2lLink.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
--opacity: .3;
}

.l2lLink:hover {
--opacity: .4;
}

.l2dLink {
--color-h: var(--d-color-h);
--color-s: var(--d-color-s);
--color-l: var(--d-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
--opacity: .1;

fill: var(--color);
opacity: var(--opacity);
stroke: none;
}

.l2dLink.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
--opacity: .3;
}

.l2dLink:hover {
--opacity: .4;
}
</style>
`
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more