deeplearning4j/deeplearning4j-ui-parent/deeplearning4j-ui-components/src/main/typescript/org/deeplearning4j/ui/components/chart/ChartTimeline.ts
/*
* ******************************************************************************
* *
* *
* * This program and the accompanying materials are made available under the
* * terms of the Apache License, Version 2.0 which is available at
* * https://www.apache.org/licenses/LICENSE-2.0.
* *
* * See the NOTICE file distributed with this work for additional
* * information regarding copyright ownership.
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* * License for the specific language governing permissions and limitations
* * under the License.
* *
* * SPDX-License-Identifier: Apache-2.0
* *****************************************************************************
*/
class ChartTimeline extends Chart implements Renderable {
private laneNames:string[];
private laneData:any[][];
private lanes:any;
private itemData:any;
private mainView:any;
private miniView:any;
private brush:any;
private x:any;
private x1:any;
private xTimeAxis:any;
private y1:any;
private y2:any;
private itemRects:any;
private rect:any;
private static MINI_LANE_HEIGHT_PX = 12;
private static ENTRY_LANE_HEIGHT_OFFSET_FRACTION:number = 0.05;
private static ENTRY_LANE_HEIGHT_TOTAL_FRACTION:number = 0.90;
private static MILLISEC_PER_MINUTE:number = 60 * 1000;
private static MILLISEC_PER_HOUR:number = 60 * ChartTimeline.MILLISEC_PER_MINUTE;
private static MILLISEC_PER_DAY:number = 24 * ChartTimeline.MILLISEC_PER_HOUR;
private static MILLISEC_PER_WEEK:number = 7 * ChartTimeline.MILLISEC_PER_DAY;
private static DEFAULT_COLOR = "LightGrey";
constructor(jsonStr:string) {
super(ComponentType.ChartTimeline, jsonStr);
var json = JSON.parse(jsonStr);
if (!json["componentType"]) json = json[ComponentType[ComponentType.ChartTimeline]];
this.laneNames = json['laneNames'];
this.laneData = json['laneData'];
}
render = (appendToObject:JQuery) => {
var instance = this;
var s:StyleChart = this.getStyle();
var margin:Margin = Style.getMargins(s);
//Format data
this.itemData = [];
var count = 0;
for (var i = 0; i < this.laneData.length; i++) {
for (var j = 0; j < this.laneData[i].length; j++) {
var obj = {};
obj["start"] = this.laneData[i][j]["startTimeMs"];
obj["end"] = this.laneData[i][j]["endTimeMs"];
obj["id"] = count++;
obj["lane"] = i;
obj["color"] = this.laneData[i][j]["color"];
obj["label"] = this.laneData[i][j]["entryLabel"];
this.itemData.push(obj);
}
}
this.lanes = [];
for (var i = 0; i < this.laneNames.length; i++) {
var obj = {};
obj["label"] = this.laneNames[i];
obj["id"] = i;
this.lanes.push(obj);
}
// Adds the svg canvas
//TODO don't hardcode these colors/attributes...
var svg = d3.select("#" + appendToObject.attr("id"))
.append("svg")
.style("stroke-width", ( s && s.getStrokeWidth() ? s.getStrokeWidth() : ChartConstants.DEFAULT_CHART_STROKE_WIDTH))
.style("fill", "none")
.attr("width", s.getWidth())
.attr("height", s.getHeight())
.append("g");
var heightExMargins = s.getHeight() - margin.top - margin.bottom;
var widthExMargins = s.getWidth() - margin.left - margin.right;
var miniHeight = this.laneNames.length * ChartTimeline.MINI_LANE_HEIGHT_PX;
var mainHeight = s.getHeight() - miniHeight - margin.top - margin.bottom - 25;
var minTime:number = d3.min(this.itemData, function (d:any) { return d.start; });
var maxTime:number = d3.max(this.itemData, function (d:any) { return d.end; });
this.x = d3.time.scale()
.domain([minTime, maxTime])
.range([0, widthExMargins]);
this.x1 = d3.time.scale().range([0, widthExMargins]);
this.y1 = d3.scale.linear().domain([0, this.laneNames.length]).range([0, mainHeight]);
this.y2 = d3.scale.linear().domain([0, this.laneNames.length]).range([0, miniHeight]);
//Add a rectangle for clipping the elements in each swimlane
this.rect = svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', widthExMargins)
.attr('height', s.getHeight() - 100);
this.mainView = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', widthExMargins)
.attr('height', mainHeight)
.attr('font-size', '12px')
.attr('font', 'sans-serif');
this.miniView = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + (mainHeight + margin.top + 25) + ')') //25 being space for ticks/time label
.attr('width', widthExMargins)
.attr('height', miniHeight)
.attr('font-size', '10px')
.attr('font', 'sans-serif');
// Horizontal lane divider lines for mainView chart:
this.mainView.append('g').selectAll('.laneLines')
.data(this.lanes)
.enter().append('line')
.attr('x1', 0)
.attr('y1', function (d:any) {
return d3.round(instance.y1(d.id)) + 0.5;
})
.attr('x2', widthExMargins)
.attr('y2', function (d:any) {
return d3.round(instance.y1(d.id)) + 0.5;
})
.attr('stroke', 'lightgray')
.attr('stroke-width', 1);
//Add labels for lane text
this.mainView.append('g').selectAll('.laneText')
.data(this.lanes)
.enter().append('text')
.text(function (d:any) {
if(d.label) return d.label;
return "";
})
.attr('x', -10)
.attr('y', function (d:any) {
return instance.y1(d.id + .5);
})
.attr('text-anchor', 'end')
.attr("font","8pt sans-serif")
.attr('fill', 'black');
// Divider lines for miniView chart
this.miniView.append('g').selectAll('.laneLines')
.data(this.lanes)
.enter().append('line')
.attr('x1', 0)
.attr('y1', function (d:any) { return d3.round(instance.y2(d.id)) + 0.5; })
.attr('x2', widthExMargins)
.attr('y2', function (d:any) { return d3.round(instance.y2(d.id)) + 0.5; })
.attr('stroke', 'gray')
.attr('stroke-width', 1.0);
//Text for mini view
this.miniView.append('g').selectAll('.laneText')
.data(this.lanes)
.enter().append('text')
.text(function (d:any) {
if(d.label) return d.label;
return "";
})
.attr('x', -10)
.attr('y', function (d:any) {
return instance.y2(d.id + .5);
})
.attr('dy', '0.5ex')
.attr('text-anchor', 'end')
.attr('fill', 'black');
// Render time axis
this.xTimeAxis = d3.svg.axis()
.scale(this.x1)
.orient('bottom')
.ticks(d3.time.days, 1)
.tickFormat(d3.time.format('%a %d'))
.tickSize(6, 0);
//Time axis
var temp:any = this.mainView.append('g')
.attr('transform', 'translate(0,' + mainHeight + ')')
// .attr('class', 'mainView axis time')
.attr('class', 'timeAxis')
.attr('fill', 'black')
.style("stroke", "black").style("stroke-width", 1.0).style("fill", "black")
.attr("font", "10px sans-serif")
.call(this.xTimeAxis);
temp.selectAll('text').style("stroke-width", 0.0).attr('stroke-width', 0.0);
// draw the itemData
this.itemRects = this.mainView.append('g')
.attr('clip-path', 'url(#clip)');
//Entries for miniView chart
this.miniView.append('g').selectAll('miniItems')
.data(this.getMiniViewPaths(this.itemData))
.enter().append('path')
.attr('class', function (d:any) {
return 'miniItem ' + d.class;
})
.attr('d', function (d:any) {
return d.path;
})
.attr('stroke', 'black')
.attr('stroke-width', 'black');
// Draw the brush selection area (default - set extent to all data)
this.miniView.append('rect')
.attr('pointer-events', 'painted')
.attr('width', widthExMargins)
.attr('height', miniHeight)
.attr('visibility', 'hidden')
.on('mouseup', this.moveBrush);
this.brush = d3.svg.brush()
.x(this.x)
.extent([minTime, maxTime])
.on("brush", this.renderChart);
this.miniView.append('g')
.attr('class', 'x brush')
.call(this.brush)
.selectAll('rect')
.attr('y', 1)
.attr('height', miniHeight - 1)
.style('fill','gray')
.style('fill-opacity','0.2')
.style('stroke','DarkSlateGray')
.style('stroke-width',1);
this.miniView.selectAll('rect.background').remove();
this.renderChart();
//Add title (if present)
if (this.title) {
var titleStyle:StyleText;
if (this.style) titleStyle = this.style.getTitleStyle();
var text = svg.append("text")
.text(this.title)
.attr("x", (s.getWidth() / 2))
.attr("y", ((margin.top - 30) / 2))
.attr("text-anchor", "middle");
if (titleStyle) {
if (titleStyle.getFont()) text.attr("font-family", titleStyle.getFont);
if (titleStyle.getFontSize() != null) text.attr("font-size", titleStyle.getFontSize() + "pt");
if (titleStyle.getUnderline() != null) text.style("text-decoration", "underline");
if (titleStyle.getColor()) text.style("fill", titleStyle.getColor);
else text.style("fill", ChartConstants.DEFAULT_TITLE_COLOR);
} else {
text.style("text-decoration", "underline");
text.style("fill", ChartConstants.DEFAULT_TITLE_COLOR);
}
}
};
renderChart = () => {
var instance:any = this;
var extent:number[] = this.brush.extent();
var minExtent:number = extent[0];
var maxExtent:number = extent[1];
var visibleItems:any = this.itemData.filter(function (d) {
return d.start < maxExtent && d.end > minExtent
});
this.miniView.select('.brush').call(this.brush.extent([minExtent, maxExtent]));
this.x1.domain([minExtent, maxExtent]);
//https://github.com/d3/d3-time-format#timeFormat
var range = maxExtent - minExtent;
if (range > 2 * ChartTimeline.MILLISEC_PER_WEEK) {
this.xTimeAxis.ticks(d3.time.mondays, 1).tickFormat(d3.time.format('%a %d'));
} else if (range > 2 * ChartTimeline.MILLISEC_PER_DAY) {
this.xTimeAxis.ticks(d3.time.days, 1).tickFormat(d3.time.format('%a %d'));
} else if (range > 2 * ChartTimeline.MILLISEC_PER_HOUR) {
this.xTimeAxis.ticks(d3.time.hours, 4).tickFormat(d3.time.format('%H %p'));
} else if (range > 2 * ChartTimeline.MILLISEC_PER_MINUTE) {
this.xTimeAxis.ticks(d3.time.minutes, 1).tickFormat(d3.time.format('%H:%M'));
} else if (range >= 30000) {
this.xTimeAxis.ticks(d3.time.seconds, 10).tickFormat(d3.time.format('%H:%M:%S'));
} else {
this.xTimeAxis.ticks(d3.time.seconds, 1).tickFormat(d3.time.format('%H:%M:%S'));
} //no d3.time.milliseconds, so ticks below 1 second are not possible? (or, at least not using same approach as here)
// Update the axis
this.mainView.select('.timeAxis').call(this.xTimeAxis);
// Update the rectangles
var rects:any = this.itemRects.selectAll('rect')
.data(visibleItems, function (d) { return d.id; })
.attr('x', function (d) { return instance.x1(d.start); })
.attr('width', function (d) { return instance.x1(d.end) - instance.x1(d.start); });
//Set attributes for mainView swimlane rectangles
rects.enter().append('rect')
.attr('x', function (d) { return instance.x1(d.start); })
.attr('y', function (d) { return instance.y1(d.lane) + ChartTimeline.ENTRY_LANE_HEIGHT_OFFSET_FRACTION * instance.y1(1) + 0.5; })
.attr('width', function (d) { return instance.x1(d.end) - instance.x1(d.start); })
.attr('height', function (d) { return ChartTimeline.ENTRY_LANE_HEIGHT_TOTAL_FRACTION * instance.y1(1); })
.attr('stroke', 'black')
.attr('fill', function(d){
if(d.color) return d.color;
return ChartTimeline.DEFAULT_COLOR;
})
.attr('stroke-width', 1);
rects.exit().remove();
// Update the item labels
var labels:any = this.itemRects.selectAll('text')
.data(visibleItems, function (d) {
return d.id;
})
.attr('x', function (d) {
return instance.x1(Math.max(d.start, minExtent)) + 2;
})
.attr('fill', 'black');
labels.enter().append('text')
.text(function (d) {
if(instance.x1(d.end) - instance.x1(d.start) <= 30) return "";
if(d.label) return d.label;
return "";
})
.attr('x', function (d) {
return instance.x1(Math.max(d.start, minExtent)) + 2;
})
.attr('y', function (d) {
return instance.y1(d.lane) + .4 * instance.y1(1) + 0.5;
})
.attr('text-anchor', 'start')
.attr('class', 'itemLabel')
.attr('fill', 'black');
labels.exit().remove();
};
moveBrush = () => {
var origin:any = d3.mouse(this.rect[0]);
var time: any = this.x.invert(origin[0]).getTime();
var halfExtent: number = (this.brush.extent()[1].getTime() - this.brush.extent()[0].getTime()) / 2;
this.brush.extent([new Date(time - halfExtent), new Date(time + halfExtent)]);
this.renderChart();
};
getMiniViewPaths = (items:any) => {
var paths = {}, d, offset = .5 * this.y2(1) + 0.5, result = [];
for (var i = 0; i < items.length; i++) {
d = items[i];
if (!paths[d.class]) paths[d.class] = '';
paths[d.class] += ['M', this.x(d.start), (this.y2(d.lane) + offset), 'H', this.x(d.end)].join(' ');
}
for (var className in paths) {
result.push({class: className, path: paths[className]});
}
return result;
}
}