From e48dca5da11aa417b01ced2d99e636563f0a5cfc Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 18 Oct 2023 14:44:49 +0100 Subject: [PATCH 1/5] WIP: Added multi-layers --- bbtimeline-background-canvas.js | 175 ++++++++++ bbtimeline-canvas.js | 95 +++++ bbtimeline-foreground-canvas.js | 125 +++++++ bbtimeline.js | 596 +++++++++++++++++--------------- bbtimeline.min.js | 2 +- demo-test.html | 29 +- 6 files changed, 733 insertions(+), 289 deletions(-) create mode 100644 bbtimeline-background-canvas.js create mode 100644 bbtimeline-canvas.js create mode 100644 bbtimeline-foreground-canvas.js diff --git a/bbtimeline-background-canvas.js b/bbtimeline-background-canvas.js new file mode 100644 index 0000000..a678a0a --- /dev/null +++ b/bbtimeline-background-canvas.js @@ -0,0 +1,175 @@ +class BBTimelineBackgroundCanvas extends BBTimelineCanvas { + constructor(parentEl, el) { + super(parentEl, el); + + const a = this; + + a.GraphRectangle = { X: 0, Y: 0, W: 0, H: 0 }; + a.Margin = 0; + a.StepHeight = 0; + a.NoStep = 0; + + a.initialiseComponents(); + } + + initialiseComponents() { + super.initialiseComponents(); + + const a = this; + + a.ClientRectangle = { + 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)) + }; + + a.GraphRectangle = { + X: a.ClientRectangle.X, + Y: a.ClientRectangle.Y, + W: a.ClientRectangle.W, + H: (a.ClientRectangle.H - a.calcXAxisHeight()) + }; + + 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(); + } + + 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(); + 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.Parent.Axis.LineWidth; + a.CTX.strokeStyle = a.Parent.Axis.LineColour1; + a.CTX.stroke(); + } + + drawXAxisTicks() { + 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.Parent.Axis.LineWidth; + + let i = 0; + + while (true) { + if (startPosX >= endPosX) { + break; + } + + a.CTX.beginPath(); + a.CTX.moveTo(startPosX, posY); + + if ((i % a.Parent.Axis.X.NoPartPerDay) == 0) { + a.CTX.lineTo(startPosX, (posY + a.Parent.Axis.X.DayLineHeight)); + a.CTX.strokeStyle = a.Parent.Axis.X.DayLineColour; + } else { + a.CTX.lineTo(startPosX, (posY + a.Parent.Axis.X.HourLineHeight)); + a.CTX.strokeStyle = a.Parent.Axis.X.HourLineColour; + } + + a.CTX.lineWidth = a.Parent.Axis.LineWidth; + a.CTX.stroke(); + + startPosX += a.Parent.Axis.X.HourLineSpace; + + i++; + } + } + + drawXAxisLabels() { + const a = this; + + const result = a.calcXAxisPositions(); + const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Parent.Axis.LineWidth; + + result.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) { + // return; + // } + + const labelSize = a.drawText(e.X, (posY + a.Parent.Axis.X.DayLineHeight), a.Parent.DateToString(date, "dd"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "center"); + + // Write month on first of the month + if (writeLabel) { + a.drawText(e.X, (posY + a.Parent.Axis.X.DayLineHeight + labelSize.Height + a.Parent.Axis.LabelSpacing), a.Parent.DateToString(date, "MMMM yyyy"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "left"); + } + }); + } + + calcXAxisPositions() { + 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.DateToString(date, a.Parent.DateParsePattern), + X: x + }); + + x += (a.Parent.Axis.X.HourLineSpace * a.Parent.Axis.X.NoPartPerDay); + date.setDate(date.getDate() + 1); + } + + return result; + } + + calcXAxisHeight() { + const a = this; + const labelSize = a.measureText(a.Parent.Axis.Font, "0"); + + return labelSize.Height + a.Parent.Axis.LabelSpacing + (a.Parent.Axis.X.DayLineHeight * 2); + } + + // OnMouseDown(sender, e, event) { + // } + + // OnClick(sender, e, event) { + // } + + +} \ No newline at end of file diff --git a/bbtimeline-canvas.js b/bbtimeline-canvas.js new file mode 100644 index 0000000..97f49a0 --- /dev/null +++ b/bbtimeline-canvas.js @@ -0,0 +1,95 @@ +class BBTimelineCanvas { + constructor(parentEl, el) { + const a = this; + + a.Parent = parentEl; + a.Container = el; + a.CTX = a.Container.getContext("2d"); + + a.ClientRectangle = { X: 0, Y: 0, W: 0, H: 0 }; + + a.initialiseComponents(); + } + + Clear() { + const a = this; + + a.CTX.clearRect(0, 0, a.CTX.canvas.width, a.CTX.canvas.height); + } + + Invalidate() { + // placeholder + } + + 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(); + } + + + 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, 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(); + } + + half(value) { + return (value / 2); + } + + 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) + }; + } + +} \ No newline at end of file diff --git a/bbtimeline-foreground-canvas.js b/bbtimeline-foreground-canvas.js new file mode 100644 index 0000000..0e4af99 --- /dev/null +++ b/bbtimeline-foreground-canvas.js @@ -0,0 +1,125 @@ +class BBTimelineForegroundCanvas extends BBTimelineCanvas { + constructor(parentEl, el) { + super(parentEl, el); + + const a = this; + + a.initialiseComponents(); + } + + initialiseComponents() { + super.initialiseComponents(); + + const a = this; + + a.Invalidate(); + } + + Invalidate() { + const a = this; + const rect = a.Parent.Layer.Background.GraphRectangle; + const margin = a.Parent.Layer.Background.Margin; + + a.Clear(); + + const startPosY = (rect.Y + a.Parent.Marker.Width); + + const visibleEvents = a.Parent.FindVisibleEvents(); + 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 + margin), markerRectangle.Y, e.Label, a.Parent.Marker.Font, a.Parent.Marker.ForeColour, "left"); + + e.Position = { X: e.Position.X, Y: posY }; + + e.HitBox = { + X: markerRectangle.X, + Y: markerRectangle.Y, + W: (markerRectangle.W + margin + labelSize.Width + a.Parent.Marker.CollisionMargin), + H: markerRectangle.H + }; + + if (a.Parent.Debug) a.drawRectangle(e.HitBox, "red"); + if (a.Parent.Debug) console.log(e); + }); + } + + + 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= endPosX) { - break; - } + // while (true) { + // if (startPosX >= endPosX) { + // break; + // } - a.ctx.beginPath(); - a.ctx.moveTo(startPosX, posY); + // 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; - } + // 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(); + // a.ctx.lineWidth = a.Axis.LineWidth; + // a.ctx.stroke(); - startPosX += a.Axis.X.HourLineSpace; + // startPosX += a.Axis.X.HourLineSpace; - i++; - } - } + // i++; + // } + // } - drawXAxisLabels() { - const a = this; + // drawXAxisLabels() { + // const a = this; - const result = a.getXAxis(); - const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Axis.LineWidth; + // 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); + // 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; - } + // 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; - // } + // // 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"); + // 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"); - } - }); - } + // // 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); + // 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(); + // 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); - } + // return a.measureMarker(x, y); + // } - drawText(x, y, label, font, foreColour, align) { - const a = this; + // drawText(x, y, label, font, foreColour, align) { + // const a = this; - a.ctx.font = font; - a.ctx.fillStyle = foreColour; + // a.ctx.font = font; + // a.ctx.fillStyle = foreColour; - const size = a.measureText(font, label); + // 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; - } + // 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)); + // a.ctx.fillText(label, x, (y + size.Height)); - return size; - } + // return size; + // } - drawRectangle(rectangle) { - const a = this; + // 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(); - } + // 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); + // drawVerticalLine(x, y) { + // const a = this; + // const linePosY = (a.GraphRectangle.Y + a.GraphRectangle.H); - if (y <= 0) { - y = (a.GraphRectangle.Y + a.HighlightLine.Width); - } + // 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(); - } + // 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); + // 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); + // let result = []; + // let x = a.GraphRectangle.X; + // let date = a.ConvertToDate(a.ShowDate); - // Rollback one day - date.setDate(date.getDate() - 1); + // // Rollback one day + // date.setDate(date.getDate() - 1); - while (true) { - if (x >= endPosX) { - break; - } + // while (true) { + // if (x >= endPosX) { + // break; + // } - result.push({ - Date: a.DateToString(date, a.DateParsePattern), - X: x - }); + // result.push({ + // Date: a.DateToString(date, a.DateParsePattern), + // X: x + // }); - x += (a.Axis.X.HourLineSpace * a.Axis.X.NoPartPerDay); - date.setDate(date.getDate() + 1); - } + // x += (a.Axis.X.HourLineSpace * a.Axis.X.NoPartPerDay); + // date.setDate(date.getDate() + 1); + // } - return result; - } + // return result; + // } - half(value) { - return (value / 2); - } + // half(value) { + // return (value / 2); + // } - measureMarker(x, y) { - const a = this; - const offset = a.half(a.Marker.Width); + // 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)) - }; + // 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; - } + // return result; + // } - measureText(font, value) { - const a = this; + // measureText(font, value) { + // const a = this; - a.ctx.font = font; - const size = a.ctx.measureText(value); + // 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 { + // Width: size.width, + // Height: size.fontBoundingBoxAscent, + // OffsetLeft: a.half(size.width), + // OffsetTop: a.half(size.fontBoundingBoxAscent) + // }; + // } ConvertToDate(value) { return new Date(Date.parse(value)); } + + + + initialiseComponents() { + const a = this; + + a.Container.innerHTML = ""; + + const canvasList = a.Container.getElementsByTagName("canvas"); + + a.Layer.Background = new BBTimelineBackgroundCanvas(a, canvasList[0]); + a.Layer.Flourish = new BBTimelineCanvas(a, canvasList[1]); + a.Layer.Markers = new BBTimelineForegroundCanvas(a, canvasList[2]); + + } + } \ No newline at end of file diff --git a/bbtimeline.min.js b/bbtimeline.min.js index 0463649..14f7735 100644 --- a/bbtimeline.min.js +++ b/bbtimeline.min.js @@ -2,4 +2,4 @@ * BBTimeline * @version v0.1.0.089 beta (2023/10/14 1658) */ -class BBTimeline{constructor(el){const a=this;a.Container=document.getElementById(el),a.DateParsePattern="yyyy-MM-dd",a.Debug=!1,a.Padding={Left:20,Top:20,Right:20,Bottom:0},a.Size={W:a.Container.innerWidth||a.Container.clientWidth,H:a.Container.innerHeight||a.Container.clientHeight},a.ctx=a.Container.getContext("2d"),a.ctx.canvas.width=a.Size.W,a.ctx.canvas.height=a.Size.H,a.Axis={LineColour1:"#CFCFCF",LineWidth:1,Font:"8pt Arial",LabelColour:"#000000",LabelSpacing:6,X:{NoPartPerDay:4,HourLineSpace:6,HourLineHeight:10,HourLineColour:"#A6A6A6",DayLineHeight:20,DayLineColour:"#282828"}},a.Marker={BorderColour:"#3A5D9C",BorderWidth:2,BackColour:"#D4DEEF",Width:10,ForeColour:"#3A5D9C",Font:"9pt Arial",CollisionMargin:8},a.HighlightLine={Colour:"#A6A6A6",Width:1},a.Events=[],a.StartDate=a.DateToString(new Date,a.DateParsePattern),a.ShowDate=a.StartDate,a.GraphRectangle=a.calcGraphRectangle(),a.Enabled=!1,a.initialiseComponents()}AddEvent(date,label,options){const a=this,_options=Object.assign(a.GenerateEventItem(),options);let event=a.FindEvent(date);null==event&&(a.Events.push(a.GenerateEvent(date)),event=a.FindEvent(date)),null!=label&&(event.Label=label),event.Events.push(_options)}CalcEndDate(){const a=this,calcdays=Math.floor(a.GraphRectangle.W/(a.Axis.X.NoPartPerDay*a.Axis.X.HourLineSpace));let date=a.ConvertToDate(a.ShowDate);return date.setDate(date.getDate()+calcdays),date.setDate(date.getDate()-1),a.DateToString(date,a.DateParsePattern)}Clear(){const a=this;a.ctx.clearRect(0,0,a.ctx.canvas.width,a.ctx.canvas.height),a.StartDate=a.DateToString(new Date,a.DateParsePattern),a.ShowDate=a.StartDate,a.Enabled=!1,a.Events=[]}DeleteMarker(date){const a=this;for(let i=0;i=e.X&&x<=x2&&y>=e.Y&&y<=y2)return a.Events[i]}return null}Load(startDate){const a=this;a.StartDate=startDate,a.Show(startDate)}Show(date){const a=this;a.ConvertToDate(date)12?date.getHours()-12:date.getHours()).toString().padStart(2,"0")),result=result.replace("mm",date.getMinutes().toString().padStart(2,"0")),result=result.replace("ss",date.getSeconds().toString().padStart(2,"0")),result=result.replace("ff",date.getMilliseconds().toString().padStart(2,"0")),result=result.replace("tt","{5}"),result=result.replace("zz",""),result=result.replace("y",date.getFullYear().toString()),result=result.replace("M",(date.getMonth()+1).toString()),result=result.replace("d",date.getDate().toString()),result=result.replace("H",date.getHours().toString()),result=result.replace("h",(date.getHours()>12?date.getHours()-12:date.getHours()).toString()),result=result.replace("m",date.getMinutes().toString()),result=result.replace("s",date.getSeconds().toString()),result=result.replace("z",""),result=result.replace("t","{6}"),result=result.replace("Z",""),result=result.replace("{1}",date.toLocaleString("default",{month:"long"})),result=result.replace("{2}",date.toLocaleString("default",{weekday:"long"})),result=result.replace("{3}",date.toLocaleString("default",{month:"short"})),result=result.replace("{4}",date.toLocaleString("default",{weekday:"short"})),result=result.replace("{5}",date.getHours()>=12?"PM":"AM"),result=result.replace("{6}",date.getHours()>=12?"P":"A"),result}OnMouseDown(sender,e,event){}OnClick(sender,e,event){}calcGraphRectangle(){const a=this,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:2*a.Marker.BorderWidth};return result.StepHeight=a.Marker.Width+result.Margin,result.NoStep=Math.floor(result.H/result.StepHeight),result}calcXAxisHeight(){const a=this,labelSize=undefined,result=undefined;return a.measureText(a.Axis.Font,"0").Height+a.Axis.LabelSpacing+2*a.Axis.X.DayLineHeight}calcMarkerPosition(x,y){const a=this;let hasMoved=!1,posY=y;for(let i=0;i=endPosX);)a.ctx.beginPath(),a.ctx.moveTo(startPosX,posY),i%a.Axis.X.NoPartPerDay==0?(a.ctx.lineTo(startPosX,posY+a.Axis.X.DayLineHeight),a.ctx.strokeStyle=a.Axis.X.DayLineColour):(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,result=a.getXAxis(),posY=a.GraphRectangle.Y+a.GraphRectangle.H+a.Axis.LineWidth;result.forEach((function(e,i){const date=a.ConvertToDate(e.Date);let writeLabel=!1;0==i?date.getDate()<25&&(writeLabel=!0):1==date.getDate()&&(writeLabel=!0);const labelSize=a.drawText(e.X,posY+a.Axis.X.DayLineHeight,a.DateToString(date,"dd"),a.Axis.Font,a.Axis.LabelColour,"center");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,width=a.Marker.Width-2*a.Marker.BorderWidth;return a.ctx.beginPath(),a.ctx.arc(x,y,width,0,2*Math.PI,!1),a.ctx.fillStyle=backColour,a.ctx.fill(),a.ctx.lineWidth=a.Marker.BorderWidth,a.ctx.strokeStyle=borderColour,a.ctx.stroke(),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-=size.OffsetLeft;break;case"right":x-=size.Width}return a.ctx.fillText(label,x,y+size.Height),size}drawRectangle(rectangle){const a=this;a.ctx.beginPath(),a.ctx.rect(rectangle.X,rectangle.Y,rectangle.W,rectangle.H),a.ctx.lineWidth=1,a.ctx.strokeStyle="red",a.ctx.stroke()}drawVerticalLine(x,y){const a=this,linePosY=a.GraphRectangle.Y+a.GraphRectangle.H;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,endPosX=a.GraphRectangle.X+a.GraphRectangle.W;let result=[],x=a.GraphRectangle.X,date=a.ConvertToDate(a.ShowDate);for(date.setDate(date.getDate()-1);!(x>=endPosX);)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,offset=a.half(a.Marker.Width),result=undefined;return{X:x-(offset+a.Marker.BorderWidth),Y:y-(offset+a.Marker.BorderWidth),W:a.Marker.Width+2*a.Marker.BorderWidth,H:a.Marker.Width+2*a.Marker.BorderWidth}}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)}}ConvertToDate(value){return new Date(Date.parse(value))}} \ No newline at end of file +class BBTimeline{constructor(el){const a=this;a.Container=document.getElementById(el),a.DateParsePattern="yyyy-MM-dd",a.Debug=!1,a.Padding={Left:20,Top:20,Right:20,Bottom:0},a.Size={W:a.Container.innerWidth||a.Container.clientWidth,H:a.Container.innerHeight||a.Container.clientHeight},a.ctx=a.Container.getContext("2d"),a.ctx.canvas.width=a.Size.W,a.ctx.canvas.height=a.Size.H,a.Axis={LineColour1:"#CFCFCF",LineWidth:1,Font:"8pt Arial",LabelColour:"#000000",LabelSpacing:6,X:{NoPartPerDay:4,HourLineSpace:6,HourLineHeight:10,HourLineColour:"#A6A6A6",DayLineHeight:20,DayLineColour:"#282828"}},a.Marker={BorderColour:"#3A5D9C",BorderWidth:2,BackColour:"#D4DEEF",Width:10,ForeColour:"#3A5D9C",Font:"9pt Arial",CollisionMargin:8},a.HighlightLine={Colour:"#A6A6A6",Width:1},a.Events=[],a.StartDate=a.DateToString(new Date,a.DateParsePattern),a.ShowDate=a.StartDate,a.GraphRectangle=a.calcGraphRectangle(),a.Enabled=!1,a.initialiseComponents()}AddEvent(date,label,options){const a=this,_options=Object.assign(a.GenerateEventItem(),options);let event=a.FindEvent(date);null==event&&(a.Events.push(a.GenerateEvent(date)),event=a.FindEvent(date)),null!=label&&(event.Label=label),event.Events.push(_options)}CalcEndDate(){const a=this,calcdays=Math.floor(a.GraphRectangle.W/(a.Axis.X.NoPartPerDay*a.Axis.X.HourLineSpace));let date=a.ConvertToDate(a.ShowDate);return date.setDate(date.getDate()+calcdays),date.setDate(date.getDate()-1),a.DateToString(date,a.DateParsePattern)}Clear(){const a=this;a.ctx.clearRect(0,0,a.ctx.canvas.width,a.ctx.canvas.height),a.StartDate=a.DateToString(new Date,a.DateParsePattern),a.ShowDate=a.StartDate,a.Enabled=!1,a.Events=[]}DeleteMarker(date){const a=this;for(let i=0;i=e.X&&x<=x2&&y>=e.Y&&y<=y2)return a.Events[i]}return null}Load(startDate){const a=this;a.StartDate=startDate,a.Show(startDate)}Show(date){const a=this;a.ConvertToDate(date)12?date.getHours()-12:date.getHours()).toString().padStart(2,"0")),result=result.replace("mm",date.getMinutes().toString().padStart(2,"0")),result=result.replace("ss",date.getSeconds().toString().padStart(2,"0")),result=result.replace("ff",date.getMilliseconds().toString().padStart(2,"0")),result=result.replace("tt","{5}"),result=result.replace("zz",""),result=result.replace("y",date.getFullYear().toString()),result=result.replace("M",(date.getMonth()+1).toString()),result=result.replace("d",date.getDate().toString()),result=result.replace("H",date.getHours().toString()),result=result.replace("h",(date.getHours()>12?date.getHours()-12:date.getHours()).toString()),result=result.replace("m",date.getMinutes().toString()),result=result.replace("s",date.getSeconds().toString()),result=result.replace("z",""),result=result.replace("t","{6}"),result=result.replace("Z",""),result=result.replace("{1}",date.toLocaleString("default",{month:"long"})),result=result.replace("{2}",date.toLocaleString("default",{weekday:"long"})),result=result.replace("{3}",date.toLocaleString("default",{month:"short"})),result=result.replace("{4}",date.toLocaleString("default",{weekday:"short"})),result=result.replace("{5}",date.getHours()>=12?"PM":"AM"),result=result.replace("{6}",date.getHours()>=12?"P":"A"),result}OnMouseDown(sender,e,event){}OnClick(sender,e,event){}calcGraphRectangle(){const a=this,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:2*a.Marker.BorderWidth};return result.StepHeight=a.Marker.Width+result.Margin,result.NoStep=Math.floor(result.H/result.StepHeight),result}calcXAxisHeight(){const a=this,labelSize=undefined,result=undefined;return a.measureText(a.Axis.Font,"0").Height+a.Axis.LabelSpacing+2*a.Axis.X.DayLineHeight}calcMarkerPosition(x,y){const a=this;let hasMoved=!1,posY=y;for(let i=0;i=endPosX);)a.ctx.beginPath(),a.ctx.moveTo(startPosX,posY),i%a.Axis.X.NoPartPerDay==0?(a.ctx.lineTo(startPosX,posY+a.Axis.X.DayLineHeight),a.ctx.strokeStyle=a.Axis.X.DayLineColour):(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,result=a.getXAxis(),posY=a.GraphRectangle.Y+a.GraphRectangle.H+a.Axis.LineWidth;result.forEach((function(e,i){const date=a.ConvertToDate(e.Date);let writeLabel=!1;0==i?date.getDate()<25&&(writeLabel=!0):1==date.getDate()&&(writeLabel=!0);const labelSize=a.drawText(e.X,posY+a.Axis.X.DayLineHeight,a.DateToString(date,"dd"),a.Axis.Font,a.Axis.LabelColour,"center");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,width=a.Marker.Width-2*a.Marker.BorderWidth;return a.ctx.beginPath(),a.ctx.arc(x,y,width,0,2*Math.PI,!1),a.ctx.fillStyle=backColour,a.ctx.fill(),a.ctx.lineWidth=a.Marker.BorderWidth,a.ctx.strokeStyle=borderColour,a.ctx.stroke(),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-=size.OffsetLeft;break;case"right":x-=size.Width}return a.ctx.fillText(label,x,y+size.Height),size}drawRectangle(rectangle){const a=this;a.ctx.beginPath(),a.ctx.rect(rectangle.X,rectangle.Y,rectangle.W,rectangle.H),a.ctx.lineWidth=1,a.ctx.strokeStyle="red",a.ctx.stroke()}drawVerticalLine(x,y){const a=this,linePosY=a.GraphRectangle.Y+a.GraphRectangle.H;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,endPosX=a.GraphRectangle.X+a.GraphRectangle.W;let result=[],x=a.GraphRectangle.X,date=a.ConvertToDate(a.ShowDate);for(date.setDate(date.getDate()-1);!(x>=endPosX);)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,offset=a.half(a.Marker.Width),result=undefined;return{X:x-(offset+a.Marker.BorderWidth),Y:y-(offset+a.Marker.BorderWidth),W:a.Marker.Width+2*a.Marker.BorderWidth,H:a.Marker.Width+2*a.Marker.BorderWidth}}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)}}ConvertToDate(value){return new Date(Date.parse(value))}} \ No newline at end of file diff --git a/demo-test.html b/demo-test.html index 26ade58..1a5feb6 100644 --- a/demo-test.html +++ b/demo-test.html @@ -11,6 +11,10 @@ + + + + @@ -19,11 +23,10 @@
- +

-

@@ -88,16 +91,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 +100,7 @@ textarea { width: 100%; } + .column1 { flex: 70%; padding: 20px; @@ -119,6 +113,17 @@ textarea { display: flex; } + +#myCanvas { + border-style: solid; + border-width: 1px; + border-color: #000000; + width: 100%; + height: 300px; + padding: 0; + margin: 0; +} + + @@ -34,6 +35,7 @@

+


@@ -50,8 +52,8 @@

- - + +


@@ -145,6 +147,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(); @@ -161,17 +171,22 @@ function ToggleDebug() { timeline1.Invalidate(true, true); } +function ToggleHotTracking() { + timeline1.EnableHotTracking = !timeline1.EnableHotTracking; + 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 + ")"); @@ -180,7 +195,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)); @@ -188,7 +203,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); @@ -204,20 +219,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; @@ -232,7 +247,7 @@ function UpdateLabel() { } function UpdateMarker() { - const visibleMarkers = timeline1.FindVisibleEvents(); + const visibleMarkers = timeline1.VisibleEvents; if (visibleMarkers.length <= 0) { LogInfo("No visible markers"); return; @@ -246,7 +261,7 @@ function UpdateMarker() { } function DeleteMarker() { - const visibleMarkers = timeline1.FindVisibleEvents(); + const visibleMarkers = timeline1.VisibleEvents; if (visibleMarkers.length <= 0) { LogInfo("No visible markers"); return; @@ -261,7 +276,7 @@ function DeleteMarker() { } function FindVisibleEvents() { - const visibleMarkers = timeline1.FindVisibleEvents(); + const visibleMarkers = timeline1.VisibleEvents; LogInfo(""); LogInfo(JSON.stringify(visibleMarkers)); -- 2.45.2 From 8663a39116cf9d0315fa43614e00b369ddfeae77 Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 20 Oct 2023 00:25:40 +0100 Subject: [PATCH 3/5] Added minified JS --- bbtimeline.min.js | 11 ++++++++++- demo-test.html | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/bbtimeline.min.js b/bbtimeline.min.js index fd5144b..11f2cb2 100644 --- a/bbtimeline.min.js +++ b/bbtimeline.min.js @@ -1,4 +1,13 @@ /** * BBTimeline - * @version v0.1.0.089 beta (2023/10/14 1658) + * @version v0.1.1.121 beta (2023/10/18 2058) */ +class BBTimelineCanvas{constructor(t,e){const n=this;n.Parent=t,n.Container=e,n.CTX=n.Container.getContext("2d"),n.ClientRectangle={X:0,Y:0,W:0,H:0},n.initialiseComponents()}initialiseComponents(){const t=this;t.Container.style.width=t.Parent.Size.W+"px",t.Container.style.height=t.Parent.Size.H+"px",t.Container.style.position="absolute",t.Container.style.border="none",t.CTX.canvas.width=t.Parent.Size.W,t.CTX.canvas.height=t.Parent.Size.H,t.Clear()}Clear(){const t=this;t.CTX.clearRect(0,0,t.CTX.canvas.width,t.CTX.canvas.height)}Invalidate(){}drawText(t,e,n,i,s,a){const o=this;o.CTX.font=i,o.CTX.fillStyle=s;const r=o.measureText(i,n);switch(a){case"center":t-=r.OffsetLeft;break;case"right":t-=r.Width}return o.CTX.fillText(n,t,e+r.Height),r}drawRectangle(t,e){const n=this;n.CTX.beginPath(),n.CTX.rect(t.X,t.Y,t.W,t.H),n.CTX.lineWidth=1,n.CTX.strokeStyle=e,n.CTX.stroke()}half(t){return t/2}measureText(t,e){const n=this;n.CTX.font=t;const i=n.CTX.measureText(e);return{Width:i.width,Height:i.fontBoundingBoxAscent,OffsetLeft:n.half(i.width),OffsetTop:n.half(i.fontBoundingBoxAscent)}}} + +class BBTimelineBackgroundCanvas extends BBTimelineCanvas{constructor(e,t){super(e,t);const a=this;a.GraphRectangle={X:0,Y:0,W:0,H:0},a.Margin=0,a.StepHeight=0,a.NoStep=0,a.initialiseComponents()}initialiseComponents(){super.initialiseComponents();const e=this;e.ClientRectangle={X:e.Parent.Padding.Left,Y:e.Parent.Padding.Top,W:e.Parent.Size.W-(e.Parent.Padding.Left+e.Parent.Padding.Right),H:e.Parent.Size.H-(e.Parent.Padding.Top+e.Parent.Padding.Bottom)},e.GraphRectangle={X:e.ClientRectangle.X,Y:e.ClientRectangle.Y,W:e.ClientRectangle.W,H:e.ClientRectangle.H-e.XAxisHeight},e.Margin=2*e.Parent.Marker.BorderWidth,e.StepHeight=e.Parent.Marker.Width+e.Margin,e.NoStep=Math.floor(e.GraphRectangle.H/e.StepHeight),e.Invalidate()}get XAxisHeight(){const e=this;return e.measureText(e.Parent.Axis.Font,"0").Height+e.Parent.Axis.LabelSpacing+2*e.Parent.Axis.X.DayLineHeight}get XAxisPositions(){const e=this,t=e.GraphRectangle.X+e.GraphRectangle.W;let a=[],n=e.GraphRectangle.X,i=e.Parent.ConvertToDate(e.Parent.ShowDate);for(i.setDate(i.getDate()-1);!(n>=t);)a.push({Date:e.Parent.DateToInternalString(i),X:n}),n+=e.Parent.Axis.X.HourLineSpace*e.Parent.Axis.X.NoPartPerDay,i.setDate(i.getDate()+1);return a}Invalidate(){const e=this;e.Clear(),e.drawAxis(),e.drawXAxisTicks(),e.drawXAxisLabels(),e.Parent.Debug&&e.drawRectangle(e.ClientRectangle,"red"),e.Parent.Debug&&e.drawRectangle(e.GraphRectangle,"red")}drawAxis(){const e=this;e.CTX.beginPath(),e.CTX.moveTo(e.GraphRectangle.X,e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.X,e.GraphRectangle.H+e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.W+e.GraphRectangle.X,e.GraphRectangle.H+e.GraphRectangle.Y),e.CTX.lineWidth=e.Parent.Axis.LineWidth,e.CTX.strokeStyle=e.Parent.Axis.LineColour1,e.CTX.stroke()}drawXAxisLabels(){const e=this,t=e.GraphRectangle.Y+e.GraphRectangle.H+e.Parent.Axis.LineWidth;e.XAxisPositions.forEach((function(a,n){const i=e.Parent.ConvertToDate(a.Date);let r=!1;0==n?i.getDate()<25&&(r=!0):1==i.getDate()&&(r=!0);const s=e.drawText(a.X,t+e.Parent.Axis.X.DayLineHeight,e.Parent.DateToString(i,"dd"),e.Parent.Axis.Font,e.Parent.Axis.LabelColour,"center");r&&e.drawText(a.X,t+e.Parent.Axis.X.DayLineHeight+s.Height+e.Parent.Axis.LabelSpacing,e.Parent.DateToString(i,"MMMM yyyy"),e.Parent.Axis.Font,e.Parent.Axis.LabelColour,"left")}))}drawXAxisTicks(){const e=this;let t=e.GraphRectangle.X;const a=e.GraphRectangle.X+e.GraphRectangle.W,n=e.GraphRectangle.Y+e.GraphRectangle.H+e.Parent.Axis.LineWidth;let i=0;for(;!(t>=a);)e.CTX.beginPath(),e.CTX.moveTo(t,n),i%e.Parent.Axis.X.NoPartPerDay==0?(e.CTX.lineTo(t,n+e.Parent.Axis.X.DayLineHeight),e.CTX.strokeStyle=e.Parent.Axis.X.DayLineColour):(e.CTX.lineTo(t,n+e.Parent.Axis.X.HourLineHeight),e.CTX.strokeStyle=e.Parent.Axis.X.HourLineColour),e.CTX.lineWidth=e.Parent.Axis.LineWidth,e.CTX.stroke(),t+=e.Parent.Axis.X.HourLineSpace,i++}} + +class BBTimelineFlourishCanvas extends BBTimelineCanvas{constructor(e,r){super(e,r)}DrawVerticalLine(e){const r=this,a=r.Parent.Layer.Background.GraphRectangle,n=a.Y+a.H;r.Clear(),r.CTX.beginPath(),r.CTX.moveTo(e,a.Y),r.CTX.lineTo(e,n-r.Parent.Marker.Line.Width),r.CTX.lineWidth=1,r.CTX.strokeStyle=r.Parent.HotTrack.Colour,r.CTX.stroke()}} + +class BBTimelineForegroundCanvas extends BBTimelineCanvas{constructor(e,r){super(e,r)}initialiseComponents(){super.initialiseComponents();const e=this;e.CTX.canvas.addEventListener("mousedown",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnMouseDown(this,r,n))}})),e.CTX.canvas.addEventListener("click",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnClick(this,r,n))}})),e.CTX.canvas.addEventListener("dblclick",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnDblClick(this,r,n))}})),e.Parent.EnableHotTracking&&e.CTX.canvas.addEventListener("mousemove",(function(r){if(!e.Parent.Enabled)return;if(!e.Parent.EnableHotTracking)return;const n={X:r.offsetX,Y:r.offsetY};e.Parent.HasInterception(e.Parent.Layer.Background.GraphRectangle,n)?(e.Parent.Debug&&console.log(n),e.Parent.DrawHotTracking(n.X),e.Parent.OnMouseMove(this,r,n)):e.Parent.DrawHotTracking(-1)})),e.Invalidate()}Invalidate(){const e=this,r=e.Parent.Layer.Background.GraphRectangle,n=e.Parent.Layer.Background.Margin;e.Clear();const t=r.Y+e.Parent.Marker.Width;e.Parent.VisibleEvents.forEach((function(r,a){let o=e.calcMarkerPosition(r.Position.X,t);e.drawVerticalLine(r.Position.X,o);const i=e.drawMarker(r.Position.X,o,r.BorderColour,r.BackColour),s=e.drawText(i.X+i.W+n,i.Y,r.Label,e.Parent.Marker.Font,e.Parent.Marker.ForeColour,"left");r.Position={X:r.Position.X,Y:o},r.HitBox={X:i.X,Y:i.Y,W:i.W+n+s.Width+e.Parent.Marker.CollisionMargin,H:i.H},e.Parent.Debug&&e.drawRectangle(r.HitBox,"red"),e.Parent.Debug&&console.log(r)}))}OnMouseDown(e){if(a.Parent.Enabled){var r=a.Parent.FindEventsByCoords(e.offsetX,e.offsetY);null!=r&&(a.Parent.Debug&&console.log(r),console.log("!"),a.Parent.OnMouseDown(this,e,r))}}calcMarkerPosition(e,r){const n=this;n.Parent.Layer.Background.GraphRectangle;let t=!1,a=r;for(let o=0;o=n.X&&e<=o&&t>=n.Y&&t<=i)return a.Events[r]}return null}Load(e){this.StartDate=e,this.Show(e)}Show(e){const t=this;t.ConvertToDate(e)12?e.getHours()-12:e.getHours()).toString().padStart(2,"0")),a=a.replace("mm",e.getMinutes().toString().padStart(2,"0")),a=a.replace("ss",e.getSeconds().toString().padStart(2,"0")),a=a.replace("ff",e.getMilliseconds().toString().padStart(2,"0")),a=a.replace("tt","{5}"),a=a.replace("zz",""),a=a.replace("y",e.getFullYear().toString()),a=a.replace("M",(e.getMonth()+1).toString()),a=a.replace("d",e.getDate().toString()),a=a.replace("H",e.getHours().toString()),a=a.replace("h",(e.getHours()>12?e.getHours()-12:e.getHours()).toString()),a=a.replace("m",e.getMinutes().toString()),a=a.replace("s",e.getSeconds().toString()),a=a.replace("z",""),a=a.replace("t","{6}"),a=a.replace("Z",""),a=a.replace("{1}",e.toLocaleString("default",{month:"long"})),a=a.replace("{2}",e.toLocaleString("default",{weekday:"long"})),a=a.replace("{3}",e.toLocaleString("default",{month:"short"})),a=a.replace("{4}",e.toLocaleString("default",{weekday:"short"})),a=a.replace("{5}",e.getHours()>=12?"PM":"AM"),a=a.replace("{6}",e.getHours()>=12?"P":"A"),a}DateToInternalString(e){return this.DateToString(e,this.DateParsePattern)}DrawHotTracking(e){const t=this;e<0?t.Layer.Flourish.Clear():t.Layer.Flourish.DrawVerticalLine(e)}ConvertToDate(e){return new Date(Date.parse(e))}HasInterception(e,t){const a=e.X+e.W,r=e.Y+e.H;return t.X>=e.X&&t.X<=a&&t.Y>=e.Y&&t.Y<=r}OnMouseDown(e,t,a){}OnMouseMove(e,t,a){}OnClick(e,t,a){}OnDblClick(e,t,a){}initialiseComponents(){const e=this;e.Container.innerHTML="";const t=e.Container.getElementsByTagName("canvas");e.Layer.Background=new BBTimelineBackgroundCanvas(e,t[0]),e.Layer.Flourish=new BBTimelineFlourishCanvas(e,t[1]),e.Layer.Markers=new BBTimelineForegroundCanvas(e,t[2])}} \ No newline at end of file diff --git a/demo-test.html b/demo-test.html index 4a80229..a239771 100644 --- a/demo-test.html +++ b/demo-test.html @@ -10,13 +10,13 @@ - + - + -- 2.45.2 From 0bbed0dfeee35b4a2e1094e91baba8119cdab5dc Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 22 Oct 2023 22:14:12 +0100 Subject: [PATCH 4/5] Changed to multi-layer for hot-tracking and other flourishes Changed default colours Added X-Axis top and bottom positions --- bbtimeline-background-canvas.js | 106 +++++++++++++++++-------- bbtimeline-canvas.js | 135 +++++++++++++++++++++++++------- bbtimeline-flourish-canvas.js | 29 ++++--- bbtimeline-foreground-canvas.js | 109 ++++++++++---------------- bbtimeline.js | 62 ++++++--------- bbtimeline.min.js | 9 --- demo-test.html | 40 ++++++++-- 7 files changed, 299 insertions(+), 191 deletions(-) diff --git a/bbtimeline-background-canvas.js b/bbtimeline-background-canvas.js index 05a3708..9f6a859 100644 --- a/bbtimeline-background-canvas.js +++ b/bbtimeline-background-canvas.js @@ -4,7 +4,6 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { const a = this; - a.GraphRectangle = { X: 0, Y: 0, W: 0, H: 0 }; a.Margin = 0; a.StepHeight = 0; a.NoStep = 0; @@ -17,20 +16,6 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { const a = this; - a.ClientRectangle = { - 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)) - }; - - a.GraphRectangle = { - X: a.ClientRectangle.X, - Y: a.ClientRectangle.Y, - W: a.ClientRectangle.W, - H: (a.ClientRectangle.H - a.XAxisHeight) - }; - a.Margin = (a.Parent.Marker.BorderWidth * 2); a.StepHeight = a.Parent.Marker.Width + a.Margin; a.NoStep = Math.floor(a.GraphRectangle.H / a.StepHeight); @@ -39,11 +24,31 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { } + 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.Height + a.Parent.Axis.LabelSpacing + (a.Parent.Axis.X.DayLineHeight * 2); + return labelSize.H + a.Parent.Axis.LabelSpacing + (a.Parent.XAxis.DayLineHeight * 2); } get XAxisPositions() { @@ -67,7 +72,7 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { X: x }); - x += (a.Parent.Axis.X.HourLineSpace * a.Parent.Axis.X.NoPartPerDay); + x += (a.Parent.XAxis.HourLineSpace * a.Parent.XAxis.NoPartPerDay); date.setDate(date.getDate() + 1); } @@ -93,9 +98,17 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { 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)); + + 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(); @@ -103,7 +116,21 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { drawXAxisLabels() { const a = this; - const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Parent.Axis.LineWidth; + 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); @@ -119,14 +146,15 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { } // if (i == 0) { - // return; + // // 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"); // } - const labelSize = a.drawText(e.X, (posY + a.Parent.Axis.X.DayLineHeight), a.Parent.DateToString(date, "dd"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "center"); - - // Write month on first of the month + // Write month (MMMM) on first of the month if (writeLabel) { - a.drawText(e.X, (posY + a.Parent.Axis.X.DayLineHeight + labelSize.Height + a.Parent.Axis.LabelSpacing), a.Parent.DateToString(date, "MMMM yyyy"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "left"); + a.drawText(e.X, posMonthY, a.Parent.DateToString(date, "MMMM yyyy"), a.Parent.Axis.Font, a.Parent.Axis.LabelColour, "left"); } }); } @@ -136,7 +164,19 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { let startPosX = a.GraphRectangle.X; const endPosX = (a.GraphRectangle.X + a.GraphRectangle.W); - const posY = (a.GraphRectangle.Y + a.GraphRectangle.H) + a.Parent.Axis.LineWidth; + 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; @@ -148,18 +188,18 @@ class BBTimelineBackgroundCanvas extends BBTimelineCanvas { a.CTX.beginPath(); a.CTX.moveTo(startPosX, posY); - if ((i % a.Parent.Axis.X.NoPartPerDay) == 0) { - a.CTX.lineTo(startPosX, (posY + a.Parent.Axis.X.DayLineHeight)); - a.CTX.strokeStyle = a.Parent.Axis.X.DayLineColour; + if ((i % a.Parent.XAxis.NoPartPerDay) == 0) { + a.CTX.lineTo(startPosX, posDayY); + a.CTX.strokeStyle = a.Parent.XAxis.DayLineColour; } else { - a.CTX.lineTo(startPosX, (posY + a.Parent.Axis.X.HourLineHeight)); - a.CTX.strokeStyle = a.Parent.Axis.X.HourLineColour; + 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.Axis.X.HourLineSpace; + startPosX += a.Parent.XAxis.HourLineSpace; i++; } diff --git a/bbtimeline-canvas.js b/bbtimeline-canvas.js index 6f2a5d3..adeeaa2 100644 --- a/bbtimeline-canvas.js +++ b/bbtimeline-canvas.js @@ -6,8 +6,6 @@ class BBTimelineCanvas { a.Container = el; a.CTX = a.Container.getContext("2d"); - a.ClientRectangle = { X: 0, Y: 0, W: 0, H: 0 }; - a.initialiseComponents(); } @@ -26,6 +24,18 @@ class BBTimelineCanvas { } + 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; @@ -36,31 +46,28 @@ class BBTimelineCanvas { // placeholder } - - drawText(x, y, label, font, foreColour, align) { + 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.font = font; - a.CTX.fillStyle = foreColour; + 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 size = a.measureText(font, label); + const result = { + X: x - (offset + borderWidth), + Y: y - (offset + borderWidth), + W: (width + calcBorderWidth), + H: (width + calcBorderWidth) + }; - 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; + return result; } drawRectangle(rectangle, colour) { @@ -73,8 +80,58 @@ class BBTimelineCanvas { 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); } @@ -86,11 +143,35 @@ class BBTimelineCanvas { const size = a.CTX.measureText(value); return { - Width: size.width, - Height: size.fontBoundingBoxAscent, - OffsetLeft: a.half(size.width), - OffsetTop: a.half(size.fontBoundingBoxAscent) + 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; + } + } \ No newline at end of file diff --git a/bbtimeline-flourish-canvas.js b/bbtimeline-flourish-canvas.js index ef38b88..8b0cc7e 100644 --- a/bbtimeline-flourish-canvas.js +++ b/bbtimeline-flourish-canvas.js @@ -2,21 +2,32 @@ class BBTimelineFlourishCanvas extends BBTimelineCanvas { constructor(parentEl, el) { super(parentEl, el); + const a = this; + + a.XPos = -1; } - DrawVerticalLine(x) { + Invalidate() { const a = this; - const rect = a.Parent.Layer.Background.GraphRectangle; - const linePosY = (rect.Y + rect.H); a.Clear(); - a.CTX.beginPath(); - a.CTX.moveTo(x, rect.Y); - a.CTX.lineTo(x, (linePosY - a.Parent.Marker.Line.Width)); - a.CTX.lineWidth = 1; - a.CTX.strokeStyle = a.Parent.HotTrack.Colour; - a.CTX.stroke(); + 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); + + } + } } \ No newline at end of file diff --git a/bbtimeline-foreground-canvas.js b/bbtimeline-foreground-canvas.js index 31fe2fb..730c9ef 100644 --- a/bbtimeline-foreground-canvas.js +++ b/bbtimeline-foreground-canvas.js @@ -63,21 +63,30 @@ class BBTimelineForegroundCanvas extends BBTimelineCanvas { return; } - if (!a.Parent.EnableHotTracking) { - return; - } - - const point = { X: e.offsetX, Y: e.offsetY }; - if (a.Parent.HasInterception(a.Parent.Layer.Background.GraphRectangle, point)) { - if (a.Parent.Debug) console.log(point); - - a.Parent.DrawHotTracking(point.X); - - a.Parent.OnMouseMove(this, e, point); + var event = a.Parent.FindEventsByCoords(e.offsetX, e.offsetY); + if (event != null) { + a.Container.style.cursor = 'pointer'; } else { - // Clear hot tracking - a.Parent.DrawHotTracking(-1); + 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(); + } + }); } @@ -92,25 +101,33 @@ class BBTimelineForegroundCanvas extends BBTimelineCanvas { a.Clear(); const startPosY = (rect.Y + a.Parent.Marker.Width); - const visibleEvents = a.Parent.VisibleEvents; + + // 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); - a.drawVerticalLine(e.Position.X, posY); + if (a.Parent.MarkerLabel.Line.Width > 0) { + a.drawVerticalLine(e.Position.X, posY, (a.Parent.Layer.Background.GraphRectangle.Y + a.Parent.Layer.Background.GraphRectangle.H), a.Parent.MarkerLabel.Line.Width, a.Parent.MarkerLabel.Line.Colour); + } - const markerRectangle = a.drawMarker(e.Position.X, posY, e.BorderColour, e.BackColour); - const labelSize = a.drawText((markerRectangle.X + markerRectangle.W + margin), markerRectangle.Y, e.Label, a.Parent.Marker.Font, a.Parent.Marker.ForeColour, "left"); + 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 }; - e.HitBox = { - X: markerRectangle.X, - Y: markerRectangle.Y, - W: (markerRectangle.W + margin + labelSize.Width + a.Parent.Marker.CollisionMargin), - H: markerRectangle.H - }; + 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); @@ -158,50 +175,4 @@ class BBTimelineForegroundCanvas extends BBTimelineCanvas { return posY; } - drawMarker(x, y, borderColour, backColour) { - const a = this; - const width = a.Parent.Marker.Width - (a.Parent.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.Parent.Marker.BorderWidth; - a.CTX.strokeStyle = borderColour; - a.CTX.stroke(); - - return a.measureMarker(x, y); - } - - drawVerticalLine(x, y) { - const a = this; - const rect = a.Parent.Layer.Background.GraphRectangle; - const linePosY = (rect.Y + rect.H); - - if (y <= 0) { - y = (rect.Y + a.Parent.Marker.Line.Width); - } - - a.CTX.beginPath(); - a.CTX.moveTo(x, y); - a.CTX.lineTo(x, (linePosY - a.Parent.Marker.Line.Width)); - a.CTX.lineWidth = a.Parent.Marker.Line.Width; - a.CTX.strokeStyle = a.Parent.Marker.Line.Colour; - a.CTX.stroke(); - } - - measureMarker(x, y) { - const a = this; - const offset = a.half(a.Parent.Marker.Width); - - const result = { - X: x - (offset + a.Parent.Marker.BorderWidth), - Y: y - (offset + a.Parent.Marker.BorderWidth), - W: (a.Parent.Marker.Width + (a.Parent.Marker.BorderWidth * 2)), - H: (a.Parent.Marker.Width + (a.Parent.Marker.BorderWidth * 2)) - }; - - return result; - } - } \ No newline at end of file diff --git a/bbtimeline.js b/bbtimeline.js index df6cb3b..e921b16 100644 --- a/bbtimeline.js +++ b/bbtimeline.js @@ -11,7 +11,7 @@ class BBTimeline { Left: 20, Top: 20, Right: 20, - Bottom: 0 + Bottom: 20 }; a.Size = { W: a.Container.innerWidth || a.Container.clientWidth, @@ -26,29 +26,32 @@ class BBTimeline { a.DateParsePattern = "yyyy-MM-dd"; a.Axis = { - LineColour1: "#CFCFCF", + LineColour1: "#000000", LineWidth: 1, Font: "8pt Arial", LabelColour: "#000000", - LabelSpacing: 6, - X: { - NoPartPerDay: 4, - HourLineSpace: 6, - HourLineHeight: 10, - HourLineColour: "#A6A6A6", - DayLineHeight: 20, - DayLineColour: "#282828" - } + LabelSpacing: 6 + }; + a.XAxis = { + NoPartPerDay: 4, + HourLineSpace: 6, + HourLineHeight: 10, + HourLineColour: "#EAEAEA", + DayLineHeight: 20, + DayLineColour: "#9E9E9E", + Position: 'top' }; a.Marker = { BorderColour: "#3A5D9C", BorderWidth: 2, BackColour: "#D4DEEF", - Width: 10, - ForeColour: "#3A5D9C", + Width: 10 + }; + a.MarkerLabel = { + Colour: "#3A5D9C", Font: "9pt Arial", - CollisionMargin: 8, + Margin: 8, Line: { Colour: "#A6A6A6", Width: 1, @@ -56,12 +59,14 @@ class BBTimeline { }; a.HotTrack = { - Colour: "#1D7F1D" + Colour: "#F57C00", + Width: 3 }; a.Events = []; a.StartDate = a.DateToInternalString(new Date()); a.ShowDate = a.StartDate; + a.ShowMarkerLabel = true; a.Enabled = false; a.Debug = false; @@ -102,7 +107,7 @@ class BBTimeline { const a = this; const clientWidth = (a.Size.W - (a.Padding.Left + a.Padding.Right)); - return Math.floor(clientWidth / (a.Axis.X.NoPartPerDay * a.Axis.X.HourLineSpace)); + return Math.floor(clientWidth / (a.XAxis.NoPartPerDay * a.XAxis.HourLineSpace)); } get VisibleStartDate() { @@ -305,15 +310,11 @@ class BBTimeline { Invalidate(redrawAxis, redrawMarkers) { const a = this; - if (redrawAxis) { - a.Layer.Background.Invalidate(); - } + if (redrawAxis) a.Layer.Background.Invalidate(); a.Layer.Flourish.Clear(); - if (redrawMarkers) { - a.Layer.Markers.Invalidate(); - } + if (redrawMarkers) a.Layer.Markers.Invalidate(); } @@ -369,27 +370,10 @@ class BBTimeline { return a.DateToString(date, a.DateParsePattern); } - DrawHotTracking(x) { - const a = this; - - if (x < 0) { - a.Layer.Flourish.Clear(); - } else { - a.Layer.Flourish.DrawVerticalLine(x); - } - } - ConvertToDate(value) { return new Date(Date.parse(value)); } - HasInterception(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)); - } - OnMouseDown(sender, e, event) { /* delegate */ } diff --git a/bbtimeline.min.js b/bbtimeline.min.js index 11f2cb2..2d55388 100644 --- a/bbtimeline.min.js +++ b/bbtimeline.min.js @@ -2,12 +2,3 @@ * BBTimeline * @version v0.1.1.121 beta (2023/10/18 2058) */ -class BBTimelineCanvas{constructor(t,e){const n=this;n.Parent=t,n.Container=e,n.CTX=n.Container.getContext("2d"),n.ClientRectangle={X:0,Y:0,W:0,H:0},n.initialiseComponents()}initialiseComponents(){const t=this;t.Container.style.width=t.Parent.Size.W+"px",t.Container.style.height=t.Parent.Size.H+"px",t.Container.style.position="absolute",t.Container.style.border="none",t.CTX.canvas.width=t.Parent.Size.W,t.CTX.canvas.height=t.Parent.Size.H,t.Clear()}Clear(){const t=this;t.CTX.clearRect(0,0,t.CTX.canvas.width,t.CTX.canvas.height)}Invalidate(){}drawText(t,e,n,i,s,a){const o=this;o.CTX.font=i,o.CTX.fillStyle=s;const r=o.measureText(i,n);switch(a){case"center":t-=r.OffsetLeft;break;case"right":t-=r.Width}return o.CTX.fillText(n,t,e+r.Height),r}drawRectangle(t,e){const n=this;n.CTX.beginPath(),n.CTX.rect(t.X,t.Y,t.W,t.H),n.CTX.lineWidth=1,n.CTX.strokeStyle=e,n.CTX.stroke()}half(t){return t/2}measureText(t,e){const n=this;n.CTX.font=t;const i=n.CTX.measureText(e);return{Width:i.width,Height:i.fontBoundingBoxAscent,OffsetLeft:n.half(i.width),OffsetTop:n.half(i.fontBoundingBoxAscent)}}} - -class BBTimelineBackgroundCanvas extends BBTimelineCanvas{constructor(e,t){super(e,t);const a=this;a.GraphRectangle={X:0,Y:0,W:0,H:0},a.Margin=0,a.StepHeight=0,a.NoStep=0,a.initialiseComponents()}initialiseComponents(){super.initialiseComponents();const e=this;e.ClientRectangle={X:e.Parent.Padding.Left,Y:e.Parent.Padding.Top,W:e.Parent.Size.W-(e.Parent.Padding.Left+e.Parent.Padding.Right),H:e.Parent.Size.H-(e.Parent.Padding.Top+e.Parent.Padding.Bottom)},e.GraphRectangle={X:e.ClientRectangle.X,Y:e.ClientRectangle.Y,W:e.ClientRectangle.W,H:e.ClientRectangle.H-e.XAxisHeight},e.Margin=2*e.Parent.Marker.BorderWidth,e.StepHeight=e.Parent.Marker.Width+e.Margin,e.NoStep=Math.floor(e.GraphRectangle.H/e.StepHeight),e.Invalidate()}get XAxisHeight(){const e=this;return e.measureText(e.Parent.Axis.Font,"0").Height+e.Parent.Axis.LabelSpacing+2*e.Parent.Axis.X.DayLineHeight}get XAxisPositions(){const e=this,t=e.GraphRectangle.X+e.GraphRectangle.W;let a=[],n=e.GraphRectangle.X,i=e.Parent.ConvertToDate(e.Parent.ShowDate);for(i.setDate(i.getDate()-1);!(n>=t);)a.push({Date:e.Parent.DateToInternalString(i),X:n}),n+=e.Parent.Axis.X.HourLineSpace*e.Parent.Axis.X.NoPartPerDay,i.setDate(i.getDate()+1);return a}Invalidate(){const e=this;e.Clear(),e.drawAxis(),e.drawXAxisTicks(),e.drawXAxisLabels(),e.Parent.Debug&&e.drawRectangle(e.ClientRectangle,"red"),e.Parent.Debug&&e.drawRectangle(e.GraphRectangle,"red")}drawAxis(){const e=this;e.CTX.beginPath(),e.CTX.moveTo(e.GraphRectangle.X,e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.X,e.GraphRectangle.H+e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.W+e.GraphRectangle.X,e.GraphRectangle.H+e.GraphRectangle.Y),e.CTX.lineWidth=e.Parent.Axis.LineWidth,e.CTX.strokeStyle=e.Parent.Axis.LineColour1,e.CTX.stroke()}drawXAxisLabels(){const e=this,t=e.GraphRectangle.Y+e.GraphRectangle.H+e.Parent.Axis.LineWidth;e.XAxisPositions.forEach((function(a,n){const i=e.Parent.ConvertToDate(a.Date);let r=!1;0==n?i.getDate()<25&&(r=!0):1==i.getDate()&&(r=!0);const s=e.drawText(a.X,t+e.Parent.Axis.X.DayLineHeight,e.Parent.DateToString(i,"dd"),e.Parent.Axis.Font,e.Parent.Axis.LabelColour,"center");r&&e.drawText(a.X,t+e.Parent.Axis.X.DayLineHeight+s.Height+e.Parent.Axis.LabelSpacing,e.Parent.DateToString(i,"MMMM yyyy"),e.Parent.Axis.Font,e.Parent.Axis.LabelColour,"left")}))}drawXAxisTicks(){const e=this;let t=e.GraphRectangle.X;const a=e.GraphRectangle.X+e.GraphRectangle.W,n=e.GraphRectangle.Y+e.GraphRectangle.H+e.Parent.Axis.LineWidth;let i=0;for(;!(t>=a);)e.CTX.beginPath(),e.CTX.moveTo(t,n),i%e.Parent.Axis.X.NoPartPerDay==0?(e.CTX.lineTo(t,n+e.Parent.Axis.X.DayLineHeight),e.CTX.strokeStyle=e.Parent.Axis.X.DayLineColour):(e.CTX.lineTo(t,n+e.Parent.Axis.X.HourLineHeight),e.CTX.strokeStyle=e.Parent.Axis.X.HourLineColour),e.CTX.lineWidth=e.Parent.Axis.LineWidth,e.CTX.stroke(),t+=e.Parent.Axis.X.HourLineSpace,i++}} - -class BBTimelineFlourishCanvas extends BBTimelineCanvas{constructor(e,r){super(e,r)}DrawVerticalLine(e){const r=this,a=r.Parent.Layer.Background.GraphRectangle,n=a.Y+a.H;r.Clear(),r.CTX.beginPath(),r.CTX.moveTo(e,a.Y),r.CTX.lineTo(e,n-r.Parent.Marker.Line.Width),r.CTX.lineWidth=1,r.CTX.strokeStyle=r.Parent.HotTrack.Colour,r.CTX.stroke()}} - -class BBTimelineForegroundCanvas extends BBTimelineCanvas{constructor(e,r){super(e,r)}initialiseComponents(){super.initialiseComponents();const e=this;e.CTX.canvas.addEventListener("mousedown",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnMouseDown(this,r,n))}})),e.CTX.canvas.addEventListener("click",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnClick(this,r,n))}})),e.CTX.canvas.addEventListener("dblclick",(function(r){if(e.Parent.Enabled){var n=e.Parent.FindEventsByCoords(r.offsetX,r.offsetY);null!=n&&(e.Parent.Debug&&console.log(n),e.Parent.OnDblClick(this,r,n))}})),e.Parent.EnableHotTracking&&e.CTX.canvas.addEventListener("mousemove",(function(r){if(!e.Parent.Enabled)return;if(!e.Parent.EnableHotTracking)return;const n={X:r.offsetX,Y:r.offsetY};e.Parent.HasInterception(e.Parent.Layer.Background.GraphRectangle,n)?(e.Parent.Debug&&console.log(n),e.Parent.DrawHotTracking(n.X),e.Parent.OnMouseMove(this,r,n)):e.Parent.DrawHotTracking(-1)})),e.Invalidate()}Invalidate(){const e=this,r=e.Parent.Layer.Background.GraphRectangle,n=e.Parent.Layer.Background.Margin;e.Clear();const t=r.Y+e.Parent.Marker.Width;e.Parent.VisibleEvents.forEach((function(r,a){let o=e.calcMarkerPosition(r.Position.X,t);e.drawVerticalLine(r.Position.X,o);const i=e.drawMarker(r.Position.X,o,r.BorderColour,r.BackColour),s=e.drawText(i.X+i.W+n,i.Y,r.Label,e.Parent.Marker.Font,e.Parent.Marker.ForeColour,"left");r.Position={X:r.Position.X,Y:o},r.HitBox={X:i.X,Y:i.Y,W:i.W+n+s.Width+e.Parent.Marker.CollisionMargin,H:i.H},e.Parent.Debug&&e.drawRectangle(r.HitBox,"red"),e.Parent.Debug&&console.log(r)}))}OnMouseDown(e){if(a.Parent.Enabled){var r=a.Parent.FindEventsByCoords(e.offsetX,e.offsetY);null!=r&&(a.Parent.Debug&&console.log(r),console.log("!"),a.Parent.OnMouseDown(this,e,r))}}calcMarkerPosition(e,r){const n=this;n.Parent.Layer.Background.GraphRectangle;let t=!1,a=r;for(let o=0;o=n.X&&e<=o&&t>=n.Y&&t<=i)return a.Events[r]}return null}Load(e){this.StartDate=e,this.Show(e)}Show(e){const t=this;t.ConvertToDate(e)12?e.getHours()-12:e.getHours()).toString().padStart(2,"0")),a=a.replace("mm",e.getMinutes().toString().padStart(2,"0")),a=a.replace("ss",e.getSeconds().toString().padStart(2,"0")),a=a.replace("ff",e.getMilliseconds().toString().padStart(2,"0")),a=a.replace("tt","{5}"),a=a.replace("zz",""),a=a.replace("y",e.getFullYear().toString()),a=a.replace("M",(e.getMonth()+1).toString()),a=a.replace("d",e.getDate().toString()),a=a.replace("H",e.getHours().toString()),a=a.replace("h",(e.getHours()>12?e.getHours()-12:e.getHours()).toString()),a=a.replace("m",e.getMinutes().toString()),a=a.replace("s",e.getSeconds().toString()),a=a.replace("z",""),a=a.replace("t","{6}"),a=a.replace("Z",""),a=a.replace("{1}",e.toLocaleString("default",{month:"long"})),a=a.replace("{2}",e.toLocaleString("default",{weekday:"long"})),a=a.replace("{3}",e.toLocaleString("default",{month:"short"})),a=a.replace("{4}",e.toLocaleString("default",{weekday:"short"})),a=a.replace("{5}",e.getHours()>=12?"PM":"AM"),a=a.replace("{6}",e.getHours()>=12?"P":"A"),a}DateToInternalString(e){return this.DateToString(e,this.DateParsePattern)}DrawHotTracking(e){const t=this;e<0?t.Layer.Flourish.Clear():t.Layer.Flourish.DrawVerticalLine(e)}ConvertToDate(e){return new Date(Date.parse(e))}HasInterception(e,t){const a=e.X+e.W,r=e.Y+e.H;return t.X>=e.X&&t.X<=a&&t.Y>=e.Y&&t.Y<=r}OnMouseDown(e,t,a){}OnMouseMove(e,t,a){}OnClick(e,t,a){}OnDblClick(e,t,a){}initialiseComponents(){const e=this;e.Container.innerHTML="";const t=e.Container.getElementsByTagName("canvas");e.Layer.Background=new BBTimelineBackgroundCanvas(e,t[0]),e.Layer.Flourish=new BBTimelineFlourishCanvas(e,t[1]),e.Layer.Markers=new BBTimelineForegroundCanvas(e,t[2])}} \ No newline at end of file diff --git a/demo-test.html b/demo-test.html index a239771..a6057f0 100644 --- a/demo-test.html +++ b/demo-test.html @@ -10,13 +10,13 @@ - + - + @@ -34,10 +34,25 @@

- -

+ +
+ +

+ +

+ +
+

+ + + +

+

+ +

+

@@ -176,6 +191,21 @@ function ToggleHotTracking() { timeline1.Invalidate(true, true); } +function ToggleShowLabel() { + timeline1.ShowMarkerLabel = !timeline1.ShowMarkerLabel; + timeline1.Invalidate(true, true); +} + +function ToggleXAxisPosition() { + timeline1.XAxis.Position = (timeline1.XAxis.Position == 'top' ? 'bottom' : 'top'); + 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); } -- 2.45.2 From e82d90c745aa339d81649225b1a806a32011fe1d Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 23 Oct 2023 01:21:53 +0100 Subject: [PATCH 5/5] Changed markers for x-axis positions --- bbtimeline-foreground-canvas.js | 17 +++++++++++++++-- bbtimeline.js | 4 ++-- demo-test.html | 11 ++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/bbtimeline-foreground-canvas.js b/bbtimeline-foreground-canvas.js index 730c9ef..1649e92 100644 --- a/bbtimeline-foreground-canvas.js +++ b/bbtimeline-foreground-canvas.js @@ -100,9 +100,15 @@ class BBTimelineForegroundCanvas extends BBTimelineCanvas { a.Clear(); - const startPosY = (rect.Y + a.Parent.Marker.Width); + 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}; @@ -111,9 +117,16 @@ class BBTimelineForegroundCanvas extends BBTimelineCanvas { 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, (a.Parent.Layer.Background.GraphRectangle.Y + a.Parent.Layer.Background.GraphRectangle.H), a.Parent.MarkerLabel.Line.Width, a.Parent.MarkerLabel.Line.Colour); + 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); diff --git a/bbtimeline.js b/bbtimeline.js index e921b16..da4f82b 100644 --- a/bbtimeline.js +++ b/bbtimeline.js @@ -11,7 +11,7 @@ class BBTimeline { Left: 20, Top: 20, Right: 20, - Bottom: 20 + Bottom: 0 }; a.Size = { W: a.Container.innerWidth || a.Container.clientWidth, @@ -39,7 +39,7 @@ class BBTimeline { HourLineColour: "#EAEAEA", DayLineHeight: 20, DayLineColour: "#9E9E9E", - Position: 'top' + Position: 'bottom' }; a.Marker = { diff --git a/demo-test.html b/demo-test.html index a6057f0..128ed04 100644 --- a/demo-test.html +++ b/demo-test.html @@ -197,7 +197,16 @@ function ToggleShowLabel() { } function ToggleXAxisPosition() { - timeline1.XAxis.Position = (timeline1.XAxis.Position == 'top' ? 'bottom' : 'top'); + 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); } -- 2.45.2