Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guide lines #300

Merged
merged 5 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export const DEFAULT_TEXTURE_RESOLUTION = 10
export const DEFAULT_EDGE_POINTS = 20
export const DEFAULT_EDGE_MINIMUM_BEZIER = 64
export const DEFAULT_HORIZONTAL_SCALE_MULTIPLIER = 0.25
export const DEFAULT_HORIZONTAL_SCALE = 1
export const DEFAULT_HORIZONTAL_SCALE = 1
export const DEFAULT_GUIDES_COUNT = 30
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking I may not need 30.. unless the user has a large screen. Might be worth calculating an amount instead of using a fixed one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also just create them on the fly when I need them so there'll only ever be as many as neccesary..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might experiment with this idea separately but will leave it for now.

export const DEFAULT_GUIDES_MIN_GAP = 260
73 changes: 73 additions & 0 deletions src/factories/guide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Container } from 'pixi.js'
import { rectangleFactory } from '@/factories/rectangle'
import { FormatDate } from '@/models/guides'
import { waitForViewport } from '@/objects'
import { waitForApplication } from '@/objects/application'
import { waitForConfig } from '@/objects/config'
import { waitForCull } from '@/objects/culling'
import { waitForFonts } from '@/objects/fonts'
import { waitForScale } from '@/objects/scale'

export type GuideFactory = Awaited<ReturnType<typeof guideFactory>>

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function guideFactory() {
const application = await waitForApplication()
const viewport = await waitForViewport()
const cull = await waitForCull()
const config = await waitForConfig()
brandonreid marked this conversation as resolved.
Show resolved Hide resolved
const { inter } = await waitForFonts()

const element = new Container()
cull.add(element)

const rectangle = await rectangleFactory()
element.addChild(rectangle)

const label = inter('')
element.addChild(label)

let currentDate: Date | undefined
let currentLabelFormatter: FormatDate

application.ticker.add(() => {
updatePosition()

if (element.height !== application.screen.height) {
renderLine()
}
})

async function render(date: Date, labelFormatter: FormatDate): Promise<void> {
currentDate = date
currentLabelFormatter = labelFormatter

renderLine()
await renderLabel(date)
}

function renderLine(): void {
rectangle.width = config.styles.guideLineWidth
rectangle.height = application.screen.height
rectangle.tint = config.styles.guideLineColor
}

function renderLabel(date: Date): void {
label.text = currentLabelFormatter(date)
label.fontSize = config.styles.guideTextSize
label.tint = config.styles.guideTextColor
label.position.set(config.styles.guideTextLeftPadding, config.styles.guideTextTopPadding)
}

async function updatePosition(): Promise<void> {
const scale = await waitForScale()
if (currentDate !== undefined) {
element.position.x = scale(currentDate) * viewport.scale._x + viewport.worldTransform.tx
}
}

return {
element,
render,
}
}
160 changes: 160 additions & 0 deletions src/factories/guides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Container } from 'pixi.js'
import { DEFAULT_GUIDES_COUNT, DEFAULT_GUIDES_MIN_GAP } from '@/consts'
import { GuideFactory, guideFactory } from '@/factories/guide'
import { FormatDate } from '@/models/guides'
import { LayoutSettings } from '@/models/layout'
import { waitForViewport } from '@/objects'
import { emitter } from '@/objects/events'
import { waitForScale } from '@/objects/scale'
import { repeat } from '@/utilities/repeat'
import { formatDateFns, labelFormats, timeIncrements } from '@/utilities/timeIncrements'

const visibleGuideBoundsMargin = 300

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function guidesFactory() {
const viewport = await waitForViewport()

const element = new Container()
const guides = new Map<number, GuideFactory>()

let paused = false

let currentIncrement = 0
let currentAnchor = 0
let labelFormatter: FormatDate = (date) => date.toLocaleTimeString()

emitter.on('viewportDateRangeUpdated', () => {
update()
})
emitter.on('layoutCreated', (layout) => onLayoutUpdate(layout))
emitter.on('layoutUpdated', (layout) => onLayoutUpdate(layout))

function render(): void {
createGuides()
}

async function createGuides(): Promise<void> {
const guideIndexes = Array.from({ length: DEFAULT_GUIDES_COUNT }, (val, i) => i)

for await (const guideIndex of guideIndexes) {
await createGuide(guideIndex)
}
}

async function createGuide(index: number): Promise<void> {
if (guides.has(index)) {
return
}

const response = await guideFactory()

element.addChild(response.element)

guides.set(index, response)
}

async function update(): Promise<void> {
if (paused) {
return
}

const scale = await waitForScale()
const left = scale.invert(viewport.left - visibleGuideBoundsMargin)
const gapDate = scale.invert(viewport.left - visibleGuideBoundsMargin + DEFAULT_GUIDES_MIN_GAP / viewport.scale.x)

if (!(left instanceof Date) || !(gapDate instanceof Date)) {
console.warn('Guides: Attempted to update guides with a non-temporal layout.')
return
}

const gap = gapDate.getTime() - left.getTime()
const { increment, getAnchor, labelFormat } = timeIncrements.find(timeSlot => timeSlot.ceiling > gap) ?? timeIncrements[0]

const anchor = getAnchor === undefined
? Math.floor(left.getTime() / increment) * increment
: getAnchor(left)

if (increment !== currentIncrement || anchor !== currentAnchor) {
currentIncrement = increment
currentAnchor = anchor
setLabelFormat(labelFormat)
}

setGuides()
}

function setLabelFormat(labelFormat: string): void {
switch (labelFormat) {
case labelFormats.minutes:
labelFormatter = formatDateFns.timeByMinutesWithDates
break
case labelFormats.date:
labelFormatter = formatDateFns.date
break
default:
labelFormatter = formatDateFns.timeBySeconds
}
}

function setGuides(): void {
const times = getGuideTimes()
const guidesStore = new Map(guides.entries())
const unused = Array.from(guidesStore.keys()).filter((time) => {
return !times.includes(time)
})

guides.clear()

for (const time of times) {
if (guidesStore.has(time)) {
const guide = guidesStore.get(time)!

guides.set(time, guide)

continue
}

const guide = guidesStore.get(unused.pop() ?? -1)

if (guide === undefined) {
console.warn('Guides: No unused guides available to render.')
continue
}

guide.render(new Date(time), labelFormatter)
guides.set(time, guide)
}
}

function getGuideTimes(): number[] {
return repeat(DEFAULT_GUIDES_COUNT, (index) => {
return currentAnchor + currentIncrement * index
})
}

function onLayoutUpdate(layout: LayoutSettings): void {
if (!layout.isTrace()) {
pauseGuides()
return
}

resumeGuides()
}

function pauseGuides(): void {
paused = true
element.visible = false
brandonreid marked this conversation as resolved.
Show resolved Hide resolved
}

function resumeGuides(): void {
paused = false
update()
element.visible = true
}

return {
element,
render,
}
}
6 changes: 6 additions & 0 deletions src/models/RunGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export type RunGraphStyles = {
nodeToggleBorderColor?: ColorSource,
nodeSelectedBorderColor?: ColorSource,
edgeColor?: string,
guideLineWidth?: number,
guideLineColor?: string,
guideTextTopPadding?: number,
guideTextLeftPadding?: number,
guideTextSize?: number,
guideTextColor?: string,
node?: (node: RunGraphNode) => RunGraphNodeStyles,
}

Expand Down
7 changes: 7 additions & 0 deletions src/models/guides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type FormatDate = (date: Date) => string

export type FormatDateFns = {
timeBySeconds: FormatDate,
timeByMinutesWithDates: FormatDate,
date: FormatDate,
}
3 changes: 3 additions & 0 deletions src/objects/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createApplication(stage: HTMLDivElement): void {
resolution: window.devicePixelRatio,
})

// for setting the viewport above the guides
application.stage.sortableChildren = true

stage.appendChild(application.view as HTMLCanvasElement)

emitter.emit('applicationCreated', application)
Expand Down
6 changes: 6 additions & 0 deletions src/objects/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const defaults: Omit<RequiredGraphConfig, 'runId' | 'fetch'> = {
nodeToggleBorderColor: '#51525C',
nodeSelectedBorderColor: 'rgba(104, 125, 155, 0.4)',
edgeColor: '#51525C',
guideLineWidth: 1,
guideLineColor: '#51525C',
guideTextTopPadding: 8,
guideTextLeftPadding: 8,
guideTextSize: 12,
guideTextColor: '#ADADAD',
node: () => ({
background: '#ffffff',
}),
Expand Down
15 changes: 15 additions & 0 deletions src/objects/guides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { guidesFactory } from '@/factories/guides'
import { waitForApplication } from '@/objects/application'

export async function startGuides(): Promise<void> {
const application = await waitForApplication()
const { element, render } = await guidesFactory()

application.stage.addChild(element)

render()
}

export function stopGuides(): void {
// nothing to stop
}
3 changes: 3 additions & 0 deletions src/objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { startCulling, stopCulling } from '@/objects/culling'
import { startEdgeCulling, stopEdgeCulling } from '@/objects/edgeCulling'
import { emitter } from '@/objects/events'
import { startFonts, stopFonts } from '@/objects/fonts'
import { startGuides, stopGuides } from '@/objects/guides'
import { startLabelCulling, stopLabelCulling } from '@/objects/labelCulling'
import { startNodes, stopNodes } from '@/objects/nodes'
import { startScale, stopScale } from '@/objects/scale'
Expand All @@ -27,6 +28,7 @@ export function start({ stage, props }: StartParameters): void {
startApplication()
startViewport(props)
startScale()
startGuides()
startNodes()
startScope()
startFonts()
Expand All @@ -45,6 +47,7 @@ export function stop(): void {
stopApplication()
stopViewport()
stopScale()
stopGuides()
stopStage()
stopNodes()
stopConfig()
Expand Down
3 changes: 3 additions & 0 deletions src/objects/viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export async function startViewport(props: RunGraphProps): Promise<void> {
passiveWheel: false,
})

// ensures the viewport is above the guides
viewport.zIndex = 1

viewport
.drag()
.pinch()
Expand Down
Loading