Compare commits

...

6 Commits

Author SHA1 Message Date
Ray
a9331cb881 Merge pull request 'release/0.1.1.121' (#3) from release/0.1.1.121 into master
Reviewed-on: Ray/BBTimeline#3
2023-11-11 14:21:18 +00:00
Ray
e82d90c745 Changed markers for x-axis positions 2023-10-23 01:21:53 +01:00
Ray
0bbed0dfee Changed to multi-layer for hot-tracking and other flourishes
Changed default colours
Added X-Axis top and bottom positions
2023-10-22 22:14:12 +01:00
Ray
8663a39116 Added minified JS 2023-10-20 00:25:40 +01:00
Ray
5637d8d3b9 Implemented multiple canvas layers 2023-10-20 00:22:29 +01:00
Ray
e48dca5da1 WIP: Added multi-layers 2023-10-18 14:44:49 +01:00
7 changed files with 849 additions and 481 deletions

View File

@ -0,0 +1,208 @@
class BBTimelineBackgroundCanvas extends BBTimelineCanvas {
constructor(parentEl, el) {
super(parentEl, el);
const a = this;
a.Margin = 0;
a.StepHeight = 0;
a.NoStep = 0;
a.initialiseComponents();
}
initialiseComponents() {
super.initialiseComponents();
const a = this;
a.Margin = (a.Parent.Marker.BorderWidth * 2);
a.StepHeight = a.Parent.Marker.Width + a.Margin;
a.NoStep = Math.floor(a.GraphRectangle.H / a.StepHeight);
a.Invalidate();
}
get GraphRectangle() {
const a = this;
if (a.Parent.XAxis.Position == 'top') {
return {
X: a.ClientRectangle.X,
Y: (a.ClientRectangle.Y + a.XAxisHeight),
W: a.ClientRectangle.W,
H: (a.ClientRectangle.H - a.XAxisHeight)
};
} else {
return {
X: a.ClientRectangle.X,
Y: a.ClientRectangle.Y,
W: a.ClientRectangle.W,
H: (a.ClientRectangle.H - a.XAxisHeight)
};
}
}
get XAxisHeight() {
const a = this;
const labelSize = a.measureText(a.Parent.Axis.Font, "0");
return labelSize.H + a.Parent.Axis.LabelSpacing + (a.Parent.XAxis.DayLineHeight * 2);
}
get XAxisPositions() {
const a = this;
const endPosX = (a.GraphRectangle.X + a.GraphRectangle.W);
let result = [];
let x = a.GraphRectangle.X;
let date = a.Parent.ConvertToDate(a.Parent.ShowDate);
// Rollback one day
date.setDate(date.getDate() - 1);
while (true) {
if (x >= endPosX) {
break;
}
result.push({
Date: a.Parent.DateToInternalString(date),
X: x
});
x += (a.Parent.XAxis.HourLineSpace * a.Parent.XAxis.NoPartPerDay);
date.setDate(date.getDate() + 1);
}
return result;
}
Invalidate() {
const a = this;
a.Clear();
a.drawAxis();
a.drawXAxisTicks();
a.drawXAxisLabels();
if (a.Parent.Debug) a.drawRectangle(a.ClientRectangle, "red");
if (a.Parent.Debug) a.drawRectangle(a.GraphRectangle, "red");
}
drawAxis() {
const a = this;
a.CTX.beginPath();
if (a.Parent.XAxis.Position == 'top') {
a.CTX.moveTo(a.GraphRectangle.X, (a.GraphRectangle.Y + a.GraphRectangle.H));
a.CTX.lineTo(a.GraphRectangle.X, a.GraphRectangle.Y);
a.CTX.lineTo((a.GraphRectangle.X + a.GraphRectangle.W), a.GraphRectangle.Y);
} else {
a.CTX.moveTo(a.GraphRectangle.X, a.GraphRectangle.Y);
a.CTX.lineTo(a.GraphRectangle.X, (a.GraphRectangle.Y +a.GraphRectangle.H));
a.CTX.lineTo((a.GraphRectangle.X + a.GraphRectangle.W), (a.GraphRectangle.Y + a.GraphRectangle.H));
}
a.CTX.lineWidth = a.Parent.Axis.LineWidth;
a.CTX.strokeStyle = a.Parent.Axis.LineColour1;
a.CTX.stroke();
}
drawXAxisLabels() {
const a = this;
const labelSize = a.measureText(a.Parent.Axis.Font, "0");
let posY = 0;
let posDayY = 0;
let posMonthY = 0;
if (a.Parent.XAxis.Position == 'top') {
posY = (a.GraphRectangle.Y - a.Parent.Axis.LineWidth) - 2;
posDayY = (posY - (labelSize.H + a.Parent.XAxis.DayLineHeight));
posMonthY = (posDayY - (labelSize.H + a.Parent.Axis.LabelSpacing));
} else {
posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Parent.Axis.LineWidth;
posDayY = (posY + a.Parent.XAxis.DayLineHeight);
posMonthY = (posDayY + labelSize.H + a.Parent.Axis.LabelSpacing);
}
a.XAxisPositions.forEach(function(e, i) {
const date = a.Parent.ConvertToDate(e.Date);
let writeLabel = false;
if ((i == 0)) {
// Don't label first entry if too close to the next month
if (date.getDate() < 25) {
writeLabel = true;
}
} else if (date.getDate() == 1) {
writeLabel = true;
}
// if (i == 0) {
// // Don't write first date
// } else {
// Write date (dd)
a.drawText(e.X, posDayY, a.Parent.DateToString(date, "dd"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "center");
// }
// Write month (MMMM) on first of the month
if (writeLabel) {
a.drawText(e.X, posMonthY, a.Parent.DateToString(date, "MMMM yyyy"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "left");
}
});
}
drawXAxisTicks() {
const a = this;
let startPosX = a.GraphRectangle.X;
const endPosX = (a.GraphRectangle.X + a.GraphRectangle.W);
let posY = 0;
let posDayY = 0;
let posHourY = 0;
if (a.Parent.XAxis.Position == 'top') {
posY = (a.GraphRectangle.Y - a.Parent.Axis.LineWidth);
posDayY = (posY - a.Parent.XAxis.DayLineHeight);
posHourY = (posY - a.Parent.XAxis.HourLineHeight);
} else {
posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Parent.Axis.LineWidth;
posDayY = (posY + a.Parent.XAxis.DayLineHeight);
posHourY = (posY + a.Parent.XAxis.HourLineHeight);
}
let i = 0;
while (true) {
if (startPosX >= endPosX) {
break;
}
a.CTX.beginPath();
a.CTX.moveTo(startPosX, posY);
if ((i % a.Parent.XAxis.NoPartPerDay) == 0) {
a.CTX.lineTo(startPosX, posDayY);
a.CTX.strokeStyle = a.Parent.XAxis.DayLineColour;
} else {
a.CTX.lineTo(startPosX, posHourY);
a.CTX.strokeStyle = a.Parent.XAxis.HourLineColour;
}
a.CTX.lineWidth = a.Parent.Axis.LineWidth;
a.CTX.stroke();
startPosX += a.Parent.XAxis.HourLineSpace;
i++;
}
}
}

177
bbtimeline-canvas.js Normal file
View File

@ -0,0 +1,177 @@
class BBTimelineCanvas {
constructor(parentEl, el) {
const a = this;
a.Parent = parentEl;
a.Container = el;
a.CTX = a.Container.getContext("2d");
a.initialiseComponents();
}
initialiseComponents() {
const a = this;
a.Container.style.width = a.Parent.Size.W + "px";
a.Container.style.height = a.Parent.Size.H + "px";
a.Container.style.position = 'absolute';
a.Container.style.border = 'none';
a.CTX.canvas.width = a.Parent.Size.W;
a.CTX.canvas.height = a.Parent.Size.H;
a.Clear();
}
get ClientRectangle() {
const a = this;
return {
X: a.Parent.Padding.Left,
Y: a.Parent.Padding.Top,
W: (a.Parent.Size.W - (a.Parent.Padding.Left + a.Parent.Padding.Right)),
H: (a.Parent.Size.H - (a.Parent.Padding.Top + a.Parent.Padding.Bottom))
};
}
Clear() {
const a = this;
a.CTX.clearRect(0, 0, a.CTX.canvas.width, a.CTX.canvas.height);
}
Invalidate() {
// placeholder
}
drawCircle(x, y, width, borderWidth, borderColour, backColour) {
const a = this;
const calcBorderWidth = (borderWidth * 2);
const calcWidth = width - calcBorderWidth;
const offset = a.half(width);
a.CTX.beginPath();
a.CTX.arc(x, y, calcWidth, 0, 2 * Math.PI, false);
a.CTX.fillStyle = backColour;
a.CTX.fill();
a.CTX.lineWidth = borderWidth;
a.CTX.strokeStyle = borderColour;
a.CTX.stroke();
const result = {
X: x - (offset + borderWidth),
Y: y - (offset + borderWidth),
W: (width + calcBorderWidth),
H: (width + calcBorderWidth)
};
return result;
}
drawRectangle(rectangle, colour) {
const a = this;
a.CTX.beginPath();
a.CTX.rect(rectangle.X, rectangle.Y, rectangle.W, rectangle.H);
//a.ctx.fillStyle = 'yellow';
//a.ctx.fill();
a.CTX.lineWidth = 1;
a.CTX.strokeStyle = colour;
a.CTX.stroke();
return rectangle;
}
drawText(x, y, label, font, foreColour, align) {
const a = this;
a.CTX.font = font;
a.CTX.fillStyle = foreColour;
let size = a.measureText(font, label);
size.Y = y;
switch (align) {
case "center":
size.X = (x - size.X);
break;
case "right":
size.X = (x - size.W);
break;
case "left":
default:
size.X = x;
break;
}
a.CTX.fillText(label, size.X, (size.Y + size.H));
return size;
}
drawVerticalLine(x, y1, y2, width, colour) {
const a = this;
a.CTX.beginPath();
a.CTX.moveTo(x, y1);
a.CTX.lineTo(x, (y2 - width));
a.CTX.lineWidth = width;
a.CTX.strokeStyle = colour;
a.CTX.stroke();
const result = {
X: x,
Y: y1,
W: width,
H: (y2 - y1)
};
return result;
}
half(value) {
return (value / 2);
}
measureText(font, value) {
const a = this;
a.CTX.font = font;
const size = a.CTX.measureText(value);
return {
W: size.width,
H: size.fontBoundingBoxAscent,
X: a.half(size.width),
Y: a.half(size.fontBoundingBoxAscent)
};
}
isPointInRectangle(rect, point) {
const x2 = (rect.X + rect.W);
const y2 = (rect.Y + rect.H);
return ((point.X >= rect.X) && (point.X <= x2) && (point.Y >= rect.Y) && (point.Y <= y2));
}
combineRectangle(rect1, rect2) {
const x2 = Math.max((rect1.X + rect1.W), (rect2.X + rect2.W));
const y2 = Math.max((rect1.Y + rect1.H), (rect2.Y + rect2.H));
const rect = {
X: Math.min(rect1.X, rect2.X),
Y: Math.min(rect1.Y, rect2.Y),
W: 0,
H: 0
};
rect.W = x2 - rect.X;
rect.H = y2 - rect.Y;
return rect;
}
}

View File

@ -0,0 +1,33 @@
class BBTimelineFlourishCanvas extends BBTimelineCanvas {
constructor(parentEl, el) {
super(parentEl, el);
const a = this;
a.XPos = -1;
}
Invalidate() {
const a = this;
a.Clear();
if (a.XPos < 0) {
a.Clear();
} else {
let posY = 0;
if (a.Parent.XAxis.Position == 'top') {
posY = a.Parent.Layer.Background.GraphRectangle.Y;
} else {
posY = (a.Parent.Layer.Background.GraphRectangle.Y + a.Parent.Layer.Background.GraphRectangle.H);
}
// a.drawVerticalLine(a.XPos, a.Parent.Layer.Background.GraphRectangle.Y, (a.Parent.Layer.Background.GraphRectangle.Y + a.Parent.Layer.Background.GraphRectangle.H), a.Parent.MarkerLabel.Line.Width, a.Parent.HotTrack.Colour);
a.drawCircle(a.XPos, posY, a.Parent.HotTrack.Width, 0, a.Parent.HotTrack.Colour, a.Parent.HotTrack.Colour);
}
}
}

View File

@ -0,0 +1,191 @@
class BBTimelineForegroundCanvas extends BBTimelineCanvas {
constructor(parentEl, el) {
super(parentEl, el);
const a = this;
// a.initialiseComponents();
}
initialiseComponents() {
super.initialiseComponents();
const a = this;
a.CTX.canvas.addEventListener('mousedown', function (e) {
if (!a.Parent.Enabled) {
return;
}
var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Parent.Debug) console.log(event);
a.Parent.OnMouseDown(this, e, event);
});
a.CTX.canvas.addEventListener('click', function (e) {
if (!a.Parent.Enabled) {
return;
}
var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Parent.Debug) console.log(event);
a.Parent.OnClick(this, e, event);
});
a.CTX.canvas.addEventListener('dblclick', function (e) {
if (!a.Parent.Enabled) {
return;
}
var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Parent.Debug) console.log(event);
a.Parent.OnDblClick(this, e, event);
});
if (a.Parent.EnableHotTracking) {
a.CTX.canvas.addEventListener('mousemove', function (e) {
if (!a.Parent.Enabled) {
return;
}
var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY);
if (event != null) {
a.Container.style.cursor = 'pointer';
} else {
a.Container.style.cursor = 'default';
}
// Hottracking
if (a.Parent.EnableHotTracking) {
const point = { X: e.offsetX, Y: e.offsetY };
if (a.isPointInRectangle(a.Parent.Layer.Background.GraphRectangle, point)) {
if (a.Parent.Debug) console.log(point);
a.Parent.Layer.Flourish.XPos = point.X;
a.Parent.OnMouseMove(this, e, event);
} else {
// Clear hot tracking
a.Parent.Layer.Flourish.XPos = -1;
}
a.Parent.Layer.Flourish.Invalidate();
}
});
}
a.Invalidate();
}
Invalidate() {
const a = this;
const rect = a.Parent.Layer.Background.GraphRectangle;
const margin = a.Parent.Layer.Background.Margin;
a.Clear();
let startPosY = 0;
const visibleEvents = a.Parent.VisibleEvents;
if (a.Parent.XAxis.Position == 'top') {
startPosY = (rect.Y + a.Parent.Marker.Width + 20);
} else {
startPosY = (rect.Y + a.Parent.Marker.Width);
}
// Clear for collisions detection
visibleEvents.forEach(function (e, i) {
e.HitBox = { X: 0, Y:0, W: 0, H: 0};
});
visibleEvents.forEach(function (e, i) {
// Calculate Y position
let posY = a.calcMarkerPosition(e.Position.X, startPosY);
let posY2 = 0;
if (a.Parent.XAxis.Position == 'top') {
posY2 = a.Parent.Layer.Background.GraphRectangle.Y;
} else {
posY2 = (a.Parent.Layer.Background.GraphRectangle.Y + a.Parent.Layer.Background.GraphRectangle.H);
}
if (a.Parent.MarkerLabel.Line.Width > 0) {
a.drawVerticalLine(e.Position.X, posY, posY2, a.Parent.MarkerLabel.Line.Width, a.Parent.MarkerLabel.Line.Colour);
}
const markerRectangle = a.drawCircle(e.Position.X, posY, a.Parent.Marker.Width, a.Parent.Marker.BorderWidth, e.BorderColour, e.BackColour);
e.Position = { X: e.Position.X, Y: posY };
if (a.Parent.ShowMarkerLabel) {
const labelRectangle = a.drawText((markerRectangle.X + markerRectangle.W + margin), markerRectangle.Y, e.Label, a.Parent.MarkerLabel.Font, a.Parent.MarkerLabel.Colour, "left");
labelRectangle.W += a.Parent.MarkerLabel.Margin;
e.HitBox = a.combineRectangle(markerRectangle, labelRectangle);
} else {
e.HitBox = markerRectangle;
}
if (a.Parent.Debug) a.drawRectangle(e.HitBox, "red");
if (a.Parent.Debug) console.log(e);
});
}
OnMouseDown(e) {
if (!a.Parent.Enabled) {
return;
}
var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Parent.Debug) console.log(event);
console.log("!");
a.Parent.OnMouseDown(this, e, event);
}
calcMarkerPosition(x, y) {
const a = this;
const rect = a.Parent.Layer.Background.GraphRectangle;
// Calculate Y position
let hasMoved = false;
let posY = y;
for (let i=0; i<a.Parent.Layer.Background.NoStep; i++)
{
posY = y + (a.Parent.Layer.Background.StepHeight * i);
var clippedEvent = a.Parent.FindEventsByCoords(x, posY);
if (clippedEvent == null) {
hasMoved = true;
break;
}
}
if (!hasMoved) {
posY = y;
}
return posY;
}
}

View File

@ -1,14 +1,12 @@
/**
* BBTimeline
* @version v0.1.0.089 beta (2023/10/14 1658)
* @version v0.1.1.121 beta (2023/10/18 2058)
*/
class BBTimeline {
constructor(el) {
const a = this;
a.Container = document.getElementById(el);
a.DateParsePattern = "yyyy-MM-dd";
a.Debug = false;
a.Padding = {
Left: 20,
Top: 20,
@ -19,56 +17,148 @@ class BBTimeline {
W: a.Container.innerWidth || a.Container.clientWidth,
H: a.Container.innerHeight || a.Container.clientHeight
};
a.Layer = {
Background: null,
Flourish: null,
Markers: null
};
a.ctx = a.Container.getContext("2d");
a.ctx.canvas.width = a.Size.W;
a.ctx.canvas.height = a.Size.H;
a.DateParsePattern = "yyyy-MM-dd";
a.Axis = {
LineColour1: "#CFCFCF",
LineColour1: "#000000",
LineWidth: 1,
Font: "8pt Arial",
LabelColour: "#000000",
LabelSpacing: 6,
X: {
LabelSpacing: 6
};
a.XAxis = {
NoPartPerDay: 4,
HourLineSpace: 6,
HourLineHeight: 10,
HourLineColour: "#A6A6A6",
HourLineColour: "#EAEAEA",
DayLineHeight: 20,
DayLineColour: "#282828"
}
DayLineColour: "#9E9E9E",
Position: 'bottom'
};
a.Marker = {
BorderColour: "#3A5D9C",
BorderWidth: 2,
BackColour: "#D4DEEF",
Width: 10,
ForeColour: "#3A5D9C",
Font: "9pt Arial",
CollisionMargin: 8
Width: 10
};
a.HighlightLine = {
a.MarkerLabel = {
Colour: "#3A5D9C",
Font: "9pt Arial",
Margin: 8,
Line: {
Colour: "#A6A6A6",
Width: 1,
}
};
a.HotTrack = {
Colour: "#F57C00",
Width: 3
};
a.Events = [];
a.StartDate = a.DateToString(new Date(), a.DateParsePattern);
a.StartDate = a.DateToInternalString(new Date());
a.ShowDate = a.StartDate;
a.GraphRectangle = a.calcGraphRectangle();
a.ShowMarkerLabel = true;
a.Enabled = false;
a.Debug = false;
a.EnableHotTracking = true;
a.initialiseComponents();
}
get CTX() {
return a.Layer.Markers.CTX;
}
get NewEvent() {
const a = this;
return {
Date: "",
Label: "",
Position: { X: 0, Y: 0 },
Events: [],
HitBox: null,
BorderColour: a.Marker.BorderColour,
BackColour: a.Marker.BackColour
};
}
get NewEventItem() {
return {
Title: "",
Description: "",
Link: "",
Tag: null
};
}
get VisibleDays() {
const a = this;
const clientWidth = (a.Size.W - (a.Padding.Left + a.Padding.Right));
return Math.floor(clientWidth / (a.XAxis.NoPartPerDay * a.XAxis.HourLineSpace));
}
get VisibleStartDate() {
const a = this;
return a.ConvertToDate(a.ShowDate);
}
get VisibleEndDate() {
const a = this;
let date = a.ConvertToDate(a.ShowDate);
date.setDate(date.getDate() + a.VisibleDays);
// Minus one for lead up
date.setDate(date.getDate() - 1);
return a.DateToString(date, a.DateParsePattern);
}
get VisibleEvents() {
const a = this;
let result = [];
a.Layer.Background.XAxisPositions.forEach(function (e) {
const event = a.FindEvent(e.Date);
if (event == null) {
return;
}
// Set offsetX on current view
event.Position.X = e.X;
result.push(event);
});
return result;
}
AddEvent(date, label, options) {
const a = this;
const _options = Object.assign(a.GenerateEventItem(), options);
const _options = Object.assign(a.NewEventItem, options);
let event = a.FindEvent(date);
if (event == null) {
a.Events.push(a.GenerateEvent(date));
let newEvent = a.NewEvent;
newEvent.Date = date;
a.Events.push(newEvent);
event = a.FindEvent(date);
}
@ -80,26 +170,14 @@ class BBTimeline {
event.Events.push(_options);
}
CalcEndDate() {
const a = this;
const calcdays = Math.floor(a.GraphRectangle.W / (a.Axis.X.NoPartPerDay * a.Axis.X.HourLineSpace));
let date = a.ConvertToDate(a.ShowDate);
date.setDate(date.getDate() + calcdays);
// Minus one for lead up
date.setDate(date.getDate() - 1);
return a.DateToString(date, a.DateParsePattern);
}
Clear() {
const a = this;
a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height);
a.Layer.Background.Clear();
a.Layer.Flourish.Clear();
a.Layer.Markers.Clear();
a.StartDate = a.DateToString(new Date(), a.DateParsePattern);
a.StartDate = a.DateToInternalString(new Date());
a.ShowDate = a.StartDate;
a.Enabled = false;
a.Events = [];
@ -120,8 +198,8 @@ class BBTimeline {
FindDatePosition(date) {
const a = this;
const points = a.Layer.Background.XAxisPositions;
const points = a.getXAxis();
for (let i=0; i<points.length; i++) {
if (points[i].Date == date){
return points[i];
@ -131,26 +209,6 @@ class BBTimeline {
return null;
}
FindVisibleEvents() {
const a = this;
let result = [];
const availableX = a.getXAxis();
availableX.forEach(function (e) {
const event = a.FindEvent(e.Date);
if (event == null) {
return;
}
// Set offsetX on current view
event.Position.X = e.X;
result.push(event);
});
return result;
}
FindEvent(date) {
const a = this;
@ -207,19 +265,19 @@ class BBTimeline {
ShowNext() {
const a = this;
let date = a.ConvertToDate(a.ShowDate);
date.setMonth(date.getMonth() + 1);
let date = a.VisibleStartDate;
date.setDate(date.getDate() + (a.VisibleDays - 1));
a.Show(a.DateToString(date, a.DateParsePattern));
a.Show(a.DateToInternalString(date));
}
ShowPrevious() {
const a = this;
let date = a.ConvertToDate(a.ShowDate);
date.setMonth(date.getMonth() - 1);
let date = a.VisibleStartDate;
date.setDate(date.getDate() - (a.VisibleDays - 1));
a.Show(a.DateToString(date, a.DateParsePattern));
a.Show(a.DateToInternalString(date));
}
UpdateLabel(date, label) {
@ -249,107 +307,15 @@ class BBTimeline {
a.Invalidate(false, true);
}
initialiseComponents() {
const a = this;
a.ctx.canvas.addEventListener('mousedown', function (e) {
if (!a.Enabled) {
return;
}
var event = a.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Debug) console.log(event);
a.OnMouseDown(this, e, event);
});
a.ctx.canvas.addEventListener('click', function (e) {
if (!a.Enabled) {
return;
}
var event = a.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
if (a.Debug) console.log(event);
a.OnClick(this, e, event);
});
}
Invalidate(redrawAxis, redrawMarkers) {
const a = this;
if (redrawAxis) {
a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height);
if (redrawAxis) a.Layer.Background.Invalidate();
a.drawAxis();
a.drawXAxis();
a.drawXAxisLabels();
}
a.Layer.Flourish.Clear();
if (redrawMarkers) {
a.clearChart();
if (redrawMarkers) a.Layer.Markers.Invalidate();
const startPosY = (a.GraphRectangle.Y + a.Marker.Width);
const visibleEvents = a.FindVisibleEvents();
if (a.Debug) console.log(visibleEvents);
visibleEvents.forEach(function (e, i) {
// Calculate Y position
let posY = a.calcMarkerPosition(e.Position.X, startPosY);
a.drawVerticalLine(e.Position.X, posY);
const markerRectangle = a.drawMarker(e.Position.X, posY, e.BorderColour, e.BackColour);
const labelSize = a.drawText((markerRectangle.X + markerRectangle.W + a.GraphRectangle.Margin), markerRectangle.Y, e.Label, a.Marker.Font, a.Marker.ForeColour, "left");
e.Position = { X: e.Position.X, Y: posY };
e.HitBox = {
X: markerRectangle.X,
Y: markerRectangle.Y,
W: (markerRectangle.W + a.GraphRectangle.Margin + labelSize.Width + a.Marker.CollisionMargin),
H: markerRectangle.H
};
if (a.Debug) a.drawRectangle(e.HitBox);
if (a.Debug) console.log(e);
});
}
if (a.Debug) a.drawRectangle(a.GraphRectangle);
}
GenerateEvent(date) {
const a = this;
return {
Date: date,
Label: "",
Position: { X: 0, Y: 0 },
Events: [],
HitBox: null,
BorderColour: a.Marker.BorderColour,
BackColour: a.Marker.BackColour
};
}
GenerateEventItem() {
return {
Title: "",
Description: "",
Link: "",
Tag: null
};
}
DateToString(date, pattern) {
@ -398,302 +364,37 @@ class BBTimeline {
return result;
}
OnMouseDown(sender, e, event) {
}
OnClick(sender, e, event) {
}
calcGraphRectangle() {
DateToInternalString(date) {
const a = this;
const xAxisHeight = a.calcXAxisHeight();
let result = {
X: a.Padding.Left,
Y: a.Padding.Top,
W: (a.Size.W - (a.Padding.Left + a.Padding.Right)),
H: (a.Size.H - (a.Padding.Top + a.Padding.Bottom) - xAxisHeight),
Margin: (a.Marker.BorderWidth * 2)
};
result.StepHeight = a.Marker.Width + result.Margin;
result.NoStep = Math.floor(result.H / result.StepHeight);
return result;
}
calcXAxisHeight() {
const a = this;
const labelSize = a.measureText(a.Axis.Font, "0");
const result = labelSize.Height + a.Axis.LabelSpacing + (a.Axis.X.DayLineHeight * 2);
return result;
}
calcMarkerPosition(x, y) {
const a = this;
// Calculate Y position
let hasMoved = false;
let posY = y;
for (let i=0; i<a.GraphRectangle.NoStep; i++)
{
posY = y + (a.GraphRectangle.StepHeight * i);
var clippedEvent = a.FindEventsByCoords(x, posY);
if (clippedEvent == null) {
hasMoved = true;
break;
}
}
if (!hasMoved) {
posY = y;
}
return posY;
}
clearChart() {
const a = this;
const rect = {
X: a.GraphRectangle.X,
Y: a.GraphRectangle.Y,
W: a.GraphRectangle.W,
H: a.GraphRectangle.H
};
rect.X += a.Axis.LineWidth;
rect.Y -= a.Padding.Top;
rect.W -= a.Axis.LineWidth;
rect.W += a.Padding.Right;
rect.H -= a.Axis.LineWidth;
rect.H += a.Padding.Top;
a.ctx.clearRect(rect.X, rect.Y, rect.W, rect.H);
if (a.Debug) a.drawRectangle(rect);
// Clear marker positions
const visibleEvents = a.FindVisibleEvents();
visibleEvents.forEach(function (e, i) {
e.Position = { X: 0, Y: 0 };
e.HitBox = null;
});
}
drawAxis() {
const a = this;
a.ctx.beginPath();
a.ctx.moveTo(a.GraphRectangle.X, a.GraphRectangle.Y);
a.ctx.lineTo(a.GraphRectangle.X, (a.GraphRectangle.H + a.GraphRectangle.Y));
a.ctx.lineTo((a.GraphRectangle.W + a.GraphRectangle.X), (a.GraphRectangle.H + a.GraphRectangle.Y));
a.ctx.lineWidth = a.Axis.LineWidth;
a.ctx.strokeStyle = a.Axis.LineColour1;
a.ctx.stroke();
}
drawXAxis() {
const a = this;
let startPosX = a.GraphRectangle.X;
const endPosX = (a.GraphRectangle.X + a.GraphRectangle.W);
const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Axis.LineWidth;
let i = 0;
while (true) {
if (startPosX >= endPosX) {
break;
}
a.ctx.beginPath();
a.ctx.moveTo(startPosX, posY);
if ((i % a.Axis.X.NoPartPerDay) == 0) {
a.ctx.lineTo(startPosX, (posY + a.Axis.X.DayLineHeight));
a.ctx.strokeStyle = a.Axis.X.DayLineColour;
} else {
a.ctx.lineTo(startPosX, (posY + a.Axis.X.HourLineHeight));
a.ctx.strokeStyle = a.Axis.X.HourLineColour;
}
a.ctx.lineWidth = a.Axis.LineWidth;
a.ctx.stroke();
startPosX += a.Axis.X.HourLineSpace;
i++;
}
}
drawXAxisLabels() {
const a = this;
const result = a.getXAxis();
const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Axis.LineWidth;
result.forEach(function(e, i) {
const date = a.ConvertToDate(e.Date);
let writeLabel = false;
if ((i == 0)) {
// Don't label first entry if too close to the next month
if (date.getDate() < 25) {
writeLabel = true;
}
} else if (date.getDate() == 1) {
writeLabel = true;
}
// if (i == 0) {
// return;
// }
const labelSize = a.drawText(e.X, (posY + a.Axis.X.DayLineHeight), a.DateToString(date, "dd"), a.Axis.Font, a.Axis.LabelColour, "center");
// Write month on first of the month
if (writeLabel) {
a.drawText(e.X, (posY + a.Axis.X.DayLineHeight + labelSize.Height + a.Axis.LabelSpacing), a.DateToString(date, "MMMM yyyy"), a.Axis.Font, a.Axis.LabelColour, "left");
}
});
}
drawMarker(x, y, borderColour, backColour) {
const a = this;
const width = a.Marker.Width - (a.Marker.BorderWidth * 2);
a.ctx.beginPath();
a.ctx.arc(x, y, width, 0, 2 * Math.PI, false);
a.ctx.fillStyle = backColour;
a.ctx.fill();
a.ctx.lineWidth = a.Marker.BorderWidth;
a.ctx.strokeStyle = borderColour;
a.ctx.stroke();
return a.measureMarker(x, y);
}
drawText(x, y, label, font, foreColour, align) {
const a = this;
a.ctx.font = font;
a.ctx.fillStyle = foreColour;
const size = a.measureText(font, label);
switch (align) {
case "center":
x = (x - size.OffsetLeft);
break;
case "right":
x = (x - size.Width);
break;
case "left":
default:
// do nothing
break;
}
a.ctx.fillText(label, x, (y + size.Height));
return size;
}
drawRectangle(rectangle) {
const a = this;
a.ctx.beginPath();
a.ctx.rect(rectangle.X, rectangle.Y, rectangle.W, rectangle.H);
//a.ctx.fillStyle = 'yellow';
//a.ctx.fill();
a.ctx.lineWidth = 1;
a.ctx.strokeStyle = 'red';
a.ctx.stroke();
}
drawVerticalLine(x, y) {
const a = this;
const linePosY = (a.GraphRectangle.Y + a.GraphRectangle.H);
if (y <= 0) {
y = (a.GraphRectangle.Y + a.HighlightLine.Width);
}
a.ctx.beginPath();
a.ctx.moveTo(x, y);
a.ctx.lineTo(x, (linePosY - a.HighlightLine.Width));
a.ctx.lineWidth = a.HighlightLine.Width;
a.ctx.strokeStyle = a.HighlightLine.Colour;
a.ctx.stroke();
}
getXAxis() {
const a = this;
const endPosX = (a.GraphRectangle.X + a.GraphRectangle.W);
let result = [];
let x = a.GraphRectangle.X;
let date = a.ConvertToDate(a.ShowDate);
// Rollback one day
date.setDate(date.getDate() - 1);
while (true) {
if (x >= endPosX) {
break;
}
result.push({
Date: a.DateToString(date, a.DateParsePattern),
X: x
});
x += (a.Axis.X.HourLineSpace * a.Axis.X.NoPartPerDay);
date.setDate(date.getDate() + 1);
}
return result;
}
half(value) {
return (value / 2);
}
measureMarker(x, y) {
const a = this;
const offset = a.half(a.Marker.Width);
const result = {
X: x - (offset + a.Marker.BorderWidth),
Y: y - (offset + a.Marker.BorderWidth),
W: (a.Marker.Width + (a.Marker.BorderWidth * 2)),
H: (a.Marker.Width + (a.Marker.BorderWidth * 2))
};
return result;
}
measureText(font, value) {
const a = this;
a.ctx.font = font;
const size = a.ctx.measureText(value);
return {
Width: size.width,
Height: size.fontBoundingBoxAscent,
OffsetLeft: a.half(size.width),
OffsetTop: a.half(size.fontBoundingBoxAscent)
};
return a.DateToString(date, a.DateParsePattern);
}
ConvertToDate(value) {
return new Date(Date.parse(value));
}
OnMouseDown(sender, e, event) { /* delegate */ }
OnMouseMove(sender, e, event) { /* delegate */ }
OnClick(sender, e, event) { /* delegate */ }
OnDblClick(sender, e, event) { /* delegate */ }
initialiseComponents() {
const a = this;
a.Container.innerHTML = "<canvas></canvas><canvas></canvas><canvas></canvas>";
const canvasList = a.Container.getElementsByTagName("canvas");
a.Layer.Background = new BBTimelineBackgroundCanvas(a, canvasList[0]);
a.Layer.Flourish = new BBTimelineFlourishCanvas(a, canvasList[1]);
a.Layer.Markers = new BBTimelineForegroundCanvas(a, canvasList[2]);
}
}

3
bbtimeline.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,11 @@
<!-- <link href="http://cdn.hiimray.co.uk/8206c600-707c-469e-8d49-a76ae35782af/bootstrap/5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<script src="bbtimeline.js"></script>
<script src="bbtimeline-canvas.js"></script>
<script src="bbtimeline-background-canvas.js"></script>
<script src="bbtimeline-flourish-canvas.js"></script>
<script src="bbtimeline-foreground-canvas.js"></script>
<!-- <script src="bbtimeline.min.js"></script> -->
<title></title>
@ -19,20 +24,35 @@
<div class="row">
<div class="column1">
<canvas id="myCanvas"></canvas>
<div id="myCanvas"></div>
<p>
<textarea id="memoBox1" readonly></textarea>
</p>
</div>
<div class="column2">
<p>
<button onclick="Clear()">Clear</button>
<button onclick="ToggleDebug()">Toggle Debug</button>
<button onclick="Refresh()">Refresh</button>
</p>
<hr />
<p>
<button onclick="ToggleDebug()">Toggle Debug</button>
</p>
<hr />
<p>
<button onclick="ToggleHotTracking()">Toggle Hot Tracking</button>
<button onclick="ToggleShowLabel()">Toggle Marker Label</button>
<button onclick="ToggleMarkerTail()">Toggle Marker Tail</button>
</p>
<p>
<button onclick="ToggleXAxisPosition()">Toggle X-Axis Position</button>
</p>
<hr />
<p>
@ -47,8 +67,8 @@
<p>
<button onclick="GoToToday()">Show Start Date</button>
<button onclick="GoToPreviousMonth()">Show Previous Month</button>
<button onclick="GoToNextMonth()">Show Next Month</button>
<button onclick="GoToPrevious()">Show Previous</button>
<button onclick="GoToNext()">Show Next</button>
</p>
<hr />
@ -88,16 +108,6 @@ body {
padding: 20px;
}
canvas {
border-style: solid;
border-width: 1px;
border-color: #000000;
width: 100%;
height: 300px;
padding: 0;
margin: 0;
}
textarea {
border-style: solid;
border-width: 1px;
@ -107,6 +117,7 @@ textarea {
width: 100%;
}
.column1 {
flex: 70%;
padding: 20px;
@ -119,6 +130,17 @@ textarea {
display: flex;
}
#myCanvas {
border-style: solid;
border-width: 1px;
border-color: #000000;
width: 100%;
height: 300px;
padding: 0;
margin: 0;
}
</style>
<script>
@ -140,6 +162,14 @@ timeline1.OnClick = function(sender, e, event) {
LogInfo(JSON.stringify(event));
LogInfo("");
}
timeline1.OnDblClick = function(sender, e, event) {
LogInfo("");
LogInfo("OnDblClick");
LogInfo(JSON.stringify(sender));
LogInfo(JSON.stringify(e));
LogInfo(JSON.stringify(event));
LogInfo("");
}
SetToday();
@ -156,17 +186,46 @@ function ToggleDebug() {
timeline1.Invalidate(true, true);
}
function ToggleHotTracking() {
timeline1.EnableHotTracking = !timeline1.EnableHotTracking;
timeline1.Invalidate(true, true);
}
function ToggleShowLabel() {
timeline1.ShowMarkerLabel = !timeline1.ShowMarkerLabel;
timeline1.Invalidate(true, true);
}
function ToggleXAxisPosition() {
if (timeline1.XAxis.Position == 'top') {
timeline1.XAxis.Position = 'bottom';
timeline1.Padding.Top = 20;
timeline1.Padding.Bottom = 0;
} else {
timeline1.XAxis.Position = 'top';
timeline1.Padding.Top = 0;
timeline1.Padding.Bottom = 20;
}
timeline1.Invalidate(true, true);
}
function ToggleMarkerTail() {
timeline1.MarkerLabel.Line.Width = ((timeline1.MarkerLabel.Line.Width <= 0) ? 1 : 0);
timeline1.Invalidate(true, true);
}
function Refresh() {
timeline1.Invalidate(true, true);
}
function SetToday() {
const msPerDay = 1000 * 60 * 60 * 24;
const startDate = timeline1.DateToString(new Date(), "yyyy-MM-dd");
const startDate = timeline1.DateToInternalString(new Date());
timeline1.Load(startDate);
const endDate = timeline1.CalcEndDate();
const endDate = timeline1.VisibleEndDate;
const noDays = Math.floor((timeline1.ConvertToDate(endDate) - timeline1.ConvertToDate(startDate)) / msPerDay);
LogInfo("Set start date to today (" + startDate + ")");
@ -175,7 +234,7 @@ function SetToday() {
function GenerateRandomMarker() {
const msPerDay = 1000 * 60 * 60 * 24;
const endDate = timeline1.CalcEndDate();
const endDate = timeline1.VisibleEndDate;
const noDays = Math.floor((timeline1.ConvertToDate(endDate) - timeline1.ConvertToDate(timeline1.ShowDate)) / msPerDay);
let randomDay = GetRandy(1, (noDays - 1));
@ -183,7 +242,7 @@ function GenerateRandomMarker() {
let date = timeline1.ConvertToDate(timeline1.ShowDate);
date.setDate(date.getDate() + randomDay);
const markerDate = timeline1.DateToString(date, timeline1.DateParsePattern);
const markerDate = timeline1.DateToInternalString(date);
const markerName = "Random Marker #" + GetRandy(10000, 99999);
@ -199,20 +258,20 @@ function GoToToday() {
LogInfo("Go to " + timeline1.ShowDate);
}
function GoToPreviousMonth() {
function GoToPrevious() {
timeline1.ShowPrevious();
LogInfo("Go to " + timeline1.ShowDate);
}
function GoToNextMonth() {
function GoToNext() {
timeline1.ShowNext();
LogInfo("Go to " + timeline1.ShowDate);
}
function UpdateLabel() {
const visibleMarkers = timeline1.FindVisibleEvents();
const visibleMarkers = timeline1.VisibleEvents;
if (visibleMarkers.length <= 0) {
LogInfo("No visible markers");
return;
@ -227,7 +286,7 @@ function UpdateLabel() {
}
function UpdateMarker() {
const visibleMarkers = timeline1.FindVisibleEvents();
const visibleMarkers = timeline1.VisibleEvents;
if (visibleMarkers.length <= 0) {
LogInfo("No visible markers");
return;
@ -241,7 +300,7 @@ function UpdateMarker() {
}
function DeleteMarker() {
const visibleMarkers = timeline1.FindVisibleEvents();
const visibleMarkers = timeline1.VisibleEvents;
if (visibleMarkers.length <= 0) {
LogInfo("No visible markers");
return;
@ -256,7 +315,7 @@ function DeleteMarker() {
}
function FindVisibleEvents() {
const visibleMarkers = timeline1.FindVisibleEvents();
const visibleMarkers = timeline1.VisibleEvents;
LogInfo("");
LogInfo(JSON.stringify(visibleMarkers));