From e48dca5da11aa417b01ced2d99e636563f0a5cfc Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 18 Oct 2023 14:44:49 +0100 Subject: [PATCH] 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; +} +