Skip to content

Instantly share code, notes, and snippets.

@tungvn
Last active January 21, 2026 03:05
Show Gist options
  • Select an option

  • Save tungvn/237fadd463f8fd72f5272c7852323f63 to your computer and use it in GitHub Desktop.

Select an option

Save tungvn/237fadd463f8fd72f5272c7852323f63 to your computer and use it in GitHub Desktop.
d3 Timeline - Typescript version (AI generated, human double check, use with careful test)

D3 Timeline - Usage Guide

A TypeScript-compatible timeline visualization library for D3 v7, upgraded from the original D3 v3 implementation.

Installation

The library is already set up in this project with D3 v7.9.0. No additional installation required.

Usage

Option 1: Import d3 with timeline (Recommended)

This is the simplest approach that gives you d3.timeline() directly:

import d3 from '@/lib/d3-with-timeline'
import { timeMonth } from 'd3-time'

// Create a timeline
const chart = d3
  .timeline()
  .width(500)
  .stack()
  .colors(d3.scaleOrdinal(['#E6DB2C', '#E63E6A', '#73C9E6']))
  .colorProperty('activity_type')
  .tickFormat({
    tickTime: timeMonth,
    tickInterval: 1,
    tickSize: 4,
  })

// Use it
d3.select('#chart').append('svg').attr('width', 500).datum(data).call(chart)

Option 2: Import timeline directly

If you prefer to import just the timeline function:

import { timeline } from '@/lib/d3-timeline'
import { select } from 'd3-selection'
import { scaleOrdinal } from 'd3-scale'

const chart = timeline().width(500).stack()

select('#chart').append('svg').datum(data).call(chart)

Data Format

Timeline expects data in the following structure:

interface TimelineTime {
  starting_time: Date | number
  ending_time: Date | number
  label?: string
  color?: string
  [key: string]: any // Additional properties for colorProperty
}

interface TimelineDatum {
  times: TimelineTime[]
  label?: string
  class?: string
  icon?: string
  [key: string]: any
}

type TimelineData = TimelineDatum[]

Example Data

const data: TimelineData = [
  {
    label: 'Construction Phase 1',
    times: [
      {
        starting_time: new Date(2023, 0, 1),
        ending_time: new Date(2023, 3, 1),
        activity_type: 'Construct',
      },
      {
        starting_time: new Date(2023, 4, 1),
        ending_time: new Date(2023, 6, 1),
        activity_type: 'Demo',
      },
    ],
  },
  {
    label: 'Construction Phase 2',
    times: [
      {
        starting_time: new Date(2023, 2, 1),
        ending_time: new Date(2023, 5, 1),
        activity_type: 'Temp',
      },
    ],
  },
]

Configuration Options

Layout & Dimensions

  • .width(pixels) - Set timeline width
  • .height(pixels) - Set timeline height
  • .margin({ left, right, top, bottom }) - Set margins
  • .itemHeight(pixels) - Height of each timeline item (default: 20)
  • .itemMargin(pixels) - Margin between items (default: 5)

Display

  • .stack() - Toggle stacked layout (groups side-by-side vs stacked)
  • .display('rect' | 'circle') - Shape of timeline items
  • .orient('top' | 'bottom') - Position of time axis

Colors

  • .colors(scaleOrdinal | string[]) - Color scale for items
  • .colorProperty(propertyName) - Property name to use for coloring (e.g., 'activity_type')
  • .background(color | function) - Background color for rows

Time Axis

  • .tickFormat({ format, tickTime, tickInterval, tickSize }) - Configure time axis
    • format: Time format function (from d3-time-format)
    • tickTime: Time interval (e.g., timeMonth, timeDay from d3-time)
    • tickInterval: Number of intervals between ticks
    • tickSize: Size of tick marks
  • .showTimeAxis() - Toggle time axis visibility
  • .showAxisTop() - Toggle axis position
  • .rotateTicks(degrees) - Rotate tick labels

Labels & Styling

  • .labelFormat(function) - Custom label formatting function
  • .labelMargin(pixels) - Margin for labels
  • .rowSeparators(color) - Add row separator lines
  • .fullLengthBackgrounds() - Toggle full-width backgrounds

Interactivity

  • .hover(callback) - Mousemove callback: (data, index, datum) => void
  • .mouseover(callback) - Mouseover callback
  • .mouseout(callback) - Mouseout callback
  • .click(callback) - Click callback
  • .scroll(callback) - Scroll callback: (x, scale) => void

Time Range

  • .beginning(date | number) - Start time (auto-calculated if not set)
  • .ending(date | number) - End time (auto-calculated if not set)
  • .relativeTime() - Toggle relative time mode

Advanced

  • .showToday() - Show today line
  • .showTodayFormat({ marginTop, marginBottom, width, color }) - Today line styling
  • .showBorderLine() - Show borders at start/end of items
  • .showBorderFormat({ marginTop, marginBottom, width, color }) - Border styling
  • .navigate(leftCallback, rightCallback) - Enable navigation buttons

TypeScript Support

The library is fully typed with comprehensive TypeScript definitions:

import { TimelineBuilder, TimelineData, TimelineDatum, TimelineTime } from '@/lib/d3-timeline'

// All methods are typed with proper return types
const chart: TimelineBuilder = timeline()
  .width(500) // Returns TimelineBuilder
  .stack() // Returns TimelineBuilder

// Data is properly typed
const data: TimelineData = [
  /* ... */
]

Migration from D3 v3

Key Changes

  1. Import Style: ES6 modules instead of global d3 object
  2. Color Scales: d3.scale.category20()scaleOrdinal(CATEGORY20_COLORS)
  3. Time Scales: d3.time.scale()scaleTime()
  4. Time Intervals: d3.time.monthstimeMonth (singular)
  5. Axis: d3.svg.axis()axisBottom() / axisTop()
  6. Zoom: d3.behavior.zoom()d3Zoom()

Breaking Changes

  • Global d3.timeline no longer exists by default
  • Must import from @/lib/d3-with-timeline to get d3.timeline()
  • Time format functions require explicit import from d3-time-format

Files

  • src/lib/d3-timeline.ts - Main timeline implementation
  • src/lib/d3-timeline-plugin.d.ts - TypeScript declarations
  • src/lib/d3-with-timeline.ts - Convenience export with d3.timeline()
// TypeScript module augmentation for d3-timeline plugin
// This allows using d3.timeline() after importing d3
import { TimelineBuilder } from './d3-timeline'
declare module 'd3' {
export function timeline(): TimelineBuilder
}
// Also augment the namespace for wildcard imports
declare global {
namespace d3 {
function timeline(): TimelineBuilder
}
}
import {
Axis,
axisBottom,
axisTop,
ScaleOrdinal,
ScaleTime,
scaleOrdinal,
scaleTime,
Selection,
select,
CountableTimeInterval,
timeHour,
timeFormat,
ZoomBehavior,
zoom as d3Zoom,
zoomIdentity,
} from 'd3'
// ============================================================================
// TypeScript Type Definitions
// ============================================================================
export type DisplayType = 'circle' | 'rect'
export interface TimelineTime {
starting_time: number | Date
ending_time: number | Date
label?: string
color?: string
id?: string
display?: DisplayType
[key: string]: any
}
export interface TimelineDatum {
times: TimelineTime[]
label?: string
class?: string
id?: string
icon?: string
[key: string]: any
}
export type TimelineData = TimelineDatum[]
export interface TimelineMargin {
left: number
right: number
top: number
bottom: number
}
export interface TimelineTickFormat {
format?: (date: Date) => string
tickTime?: CountableTimeInterval
tickInterval?: number
tickSize?: number
tickValues?: Date[] | null
numTicks?: number
}
export interface TimelineAxisTickFormat {
stroke: string
spacing: string
}
export interface TimelineLineFormat {
marginTop: number
marginBottom: number
width: number
color: string | ScaleOrdinal<string, string>
}
export type TimelineCallback<T = any> = (
data: TimelineTime,
index: number,
datum: TimelineDatum,
) => T
export interface TimelineBuilder {
(selection: Selection<SVGSVGElement, TimelineData, any, any>): void
margin(margin: TimelineMargin): TimelineBuilder
orient(orientation: 'bottom' | 'top'): TimelineBuilder
itemHeight(height: number): TimelineBuilder
itemMargin(margin: number): TimelineBuilder
navMargin(margin: number): TimelineBuilder
height(height: number): TimelineBuilder
width(width: number): TimelineBuilder
display(displayType: DisplayType): TimelineBuilder
labelFormat(formatter: (label: string) => string): TimelineBuilder
tickFormat(format: TimelineTickFormat): TimelineBuilder
hover(callback: TimelineCallback<void>): TimelineBuilder
mouseover(callback: TimelineCallback<void>): TimelineBuilder
mouseout(callback: TimelineCallback<void>): TimelineBuilder
click(callback: TimelineCallback<void>): TimelineBuilder
scroll(callback: (x: number, scale: ScaleTime<number, number>) => void): TimelineBuilder
colors(colorScale: ScaleOrdinal<string, string> | string[]): TimelineBuilder
beginning(time: number | Date): TimelineBuilder
ending(time: number | Date): TimelineBuilder
labelMargin(margin: number): TimelineBuilder
rotateTicks(degrees: number): TimelineBuilder
stack(): TimelineBuilder
relativeTime(): TimelineBuilder
showBorderLine(): TimelineBuilder
showBorderFormat(format: TimelineLineFormat): TimelineBuilder
showToday(): TimelineBuilder
showTodayFormat(format: TimelineLineFormat): TimelineBuilder
colorProperty(property: string | null): TimelineBuilder
rowSeparators(color: string | null): TimelineBuilder
background(
color: string | null | ((datum: TimelineDatum, index: number) => string),
): TimelineBuilder
showTimeAxis(): TimelineBuilder
showAxisTop(): TimelineBuilder
showAxisCalendarYear(): TimelineBuilder
showTimeAxisTick(): TimelineBuilder
fullLengthBackgrounds(): TimelineBuilder
showTimeAxisTickFormat(format: TimelineAxisTickFormat): TimelineBuilder
showAxisHeaderBackground(bgColor?: string): TimelineBuilder
navigate(
navigateBackwards: (beginning: number | Date, chartData: TimelineData) => void,
navigateForwards: (ending: number | Date, chartData: TimelineData) => void,
): TimelineBuilder
}
// ============================================================================
// D3 v7 Timeline Implementation
// ============================================================================
const DISPLAY_TYPES: DisplayType[] = ['circle', 'rect']
// Extended color scheme (20 colors) similar to D3 v3's category20
const CATEGORY20_COLORS = [
'#1f77b4',
'#aec7e8',
'#ff7f0e',
'#ffbb78',
'#2ca02c',
'#98df8a',
'#d62728',
'#ff9896',
'#9467bd',
'#c5b0d5',
'#8c564b',
'#c49c94',
'#e377c2',
'#f7b6d2',
'#7f7f7f',
'#c7c7c7',
'#bcbd22',
'#dbdb8d',
'#17becf',
'#9edae5',
]
export function timeline(): TimelineBuilder {
let hover: TimelineCallback<void> = () => {}
let mouseover: TimelineCallback<void> = () => {}
let mouseout: TimelineCallback<void> = () => {}
let click: TimelineCallback<void> = () => {}
let scroll: (x: number, scale: ScaleTime<number, number>) => void = () => {}
let labelFunction: (label: string) => string = (label) => label
let navigateLeft: (beginning: number | Date, chartData: TimelineData) => void = () => {}
let navigateRight: (ending: number | Date, chartData: TimelineData) => void = () => {}
let orient: 'bottom' | 'top' = 'bottom'
let width: number | null = null
let height: number | null = null
let rowSeparatorsColor: string | null = null
let backgroundColor: string | null | ((datum: TimelineDatum, index: number) => string) = null
let tickFormat: TimelineTickFormat = {
format: timeFormat('%I %p'),
tickTime: timeHour,
tickInterval: 1,
tickSize: 6,
tickValues: null,
}
let colorCycle: ScaleOrdinal<string, string> = scaleOrdinal(CATEGORY20_COLORS)
let colorPropertyName: string | null = null
let display: DisplayType = 'rect'
let beginning: number | Date = 0
let labelMargin = 0
let ending: number | Date = 0
let margin: TimelineMargin = { left: 30, right: 30, top: 30, bottom: 30 }
let stacked = false
let rotateTicks: number | boolean = false
let timeIsRelative = false
let fullLengthBackgrounds = false
let itemHeight = 20
let itemMargin = 5
let navMargin = 60
let showTimeAxis = true
let showAxisTop = false
let showTodayLine = false
let timeAxisTick = false
let timeAxisTickFormat: TimelineAxisTickFormat = { stroke: 'stroke-dasharray', spacing: '4 10' }
let showTodayFormat: TimelineLineFormat = {
marginTop: 25,
marginBottom: 0,
width: 1,
color: '#1f77b4',
}
let showBorderLine = false
let showBorderFormat: TimelineLineFormat = {
marginTop: 25,
marginBottom: 0,
width: 1,
color: '#1f77b4',
}
let showAxisHeaderBackground = false
let showAxisNav = false
let showAxisCalendarYear = false
let axisBgColor = 'white'
let chartData: TimelineData = []
// ============================================================================
// Helper Functions
// ============================================================================
const appendTimeAxis = (
g: Selection<SVGGElement, TimelineData, any, any>,
xAxis: Axis<Date | import('d3').NumberValue>,
yPosition: number,
) => {
if (showAxisHeaderBackground) {
appendAxisHeaderBackground(g, 0, 0)
}
if (showAxisNav) {
appendTimeAxisNav(g)
}
g.append('g').attr('class', 'axis').attr('transform', `translate(0, ${yPosition})`).call(xAxis)
}
const appendTimeAxisCalendarYear = (nav: Selection<SVGGElement, any, any, any>) => {
const beginningDate = beginning instanceof Date ? beginning : new Date(beginning)
const endingDate = ending instanceof Date ? ending : new Date(ending)
let calendarLabel = beginningDate.getFullYear().toString()
if (beginningDate.getFullYear() !== endingDate.getFullYear()) {
calendarLabel = `${beginningDate.getFullYear()}-${endingDate.getFullYear()}`
}
nav
.append('text')
.attr('transform', 'translate(20, 0)')
.attr('x', 0)
.attr('y', 14)
.attr('class', 'calendarYear')
.text(calendarLabel)
}
const appendTimeAxisNav = (g: Selection<SVGGElement, TimelineData, any, any>) => {
const timelineBlocks = 6
const leftNavMargin = margin.left - navMargin
const w = width as number
const incrementValue = (w - margin.left) / timelineBlocks
const rightNavMargin = w - margin.right - incrementValue + navMargin
const nav = g.append('g').attr('class', 'axis').attr('transform', 'translate(0, 20)')
if (showAxisCalendarYear) {
appendTimeAxisCalendarYear(nav)
}
nav
.append('text')
.attr('transform', `translate(${leftNavMargin}, 0)`)
.attr('x', 0)
.attr('y', 14)
.attr('class', 'chevron')
.text('<')
.on('click', () => navigateLeft(beginning, chartData))
nav
.append('text')
.attr('transform', `translate(${rightNavMargin}, 0)`)
.attr('x', 0)
.attr('y', 14)
.attr('class', 'chevron')
.text('>')
.on('click', () => navigateRight(ending, chartData))
}
const appendAxisHeaderBackground = (
g: Selection<SVGGElement, TimelineData, any, any>,
xAxis: number,
yAxis: number,
) => {
g.insert('rect')
.attr('class', 'row-green-bar')
.attr('x', xAxis)
.attr('width', width as number)
.attr('y', yAxis)
.attr('height', itemHeight)
.attr('fill', axisBgColor)
}
const appendTimeAxisTick = (
g: Selection<SVGGElement, TimelineData, any, any>,
xAxis: Axis<Date | import('d3').NumberValue>,
maxStack: number,
) => {
g.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${margin.top + (itemHeight + itemMargin) * maxStack})`)
.attr(timeAxisTickFormat.stroke, timeAxisTickFormat.spacing)
.call(
xAxis
.tickFormat(() => '')
.tickSize(-(margin.top + (itemHeight + itemMargin) * (maxStack - 1) + 3)),
)
}
const appendBackgroundBar = (
yAxisMapping: Record<number, number>,
index: number,
g: Selection<SVGGElement, TimelineData, any, any>,
data: TimelineTime[],
datum: TimelineDatum,
) => {
const greenbarYAxis = (itemHeight + itemMargin) * yAxisMapping[index] + margin.top
const w = width as number
g.selectAll('svg')
.data(data)
.enter()
.insert('rect')
.attr('class', 'row-green-bar')
.attr('x', fullLengthBackgrounds ? 0 : margin.left)
.attr('width', fullLengthBackgrounds ? w : w - margin.right - margin.left)
.attr('y', greenbarYAxis)
.attr('height', itemHeight)
.attr(
'fill',
backgroundColor instanceof Function ? backgroundColor(datum, index) : backgroundColor,
)
}
const appendLabel = (
gParent: Selection<SVGSVGElement, TimelineData, any, any>,
yAxisMapping: Record<number, number>,
index: number,
hasLabel: boolean,
datum: TimelineDatum,
) => {
const fullItemHeight = itemHeight + itemMargin
const rowsDown = margin.top + fullItemHeight / 2 + fullItemHeight * (yAxisMapping[index] || 1)
gParent
.append('text')
.attr('class', 'timeline-label')
.attr('transform', `translate(${labelMargin}, ${rowsDown})`)
.text(hasLabel ? labelFunction(datum.label!) : datum.id!)
.on('click', () => click({} as TimelineTime, index, datum))
}
// ============================================================================
// Main Timeline Function
// ============================================================================
function timelineFunction(gParent: Selection<SVGSVGElement, TimelineData, any, any>) {
const g = gParent.append('g')
const gParentNode = gParent.node()
if (!gParentNode) return
const gParentSize = gParentNode.getBoundingClientRect()
const gParentItem = select(gParentNode)
const yAxisMapping: Record<number, number> = {}
let maxStack = 1
let minTime: number | Date = 0
let maxTime: number | Date = 0
setWidth()
// Check if the user wants relative time
// If so, subtract the first timestamp from each subsequent timestamps
if (timeIsRelative) {
let originTime: number | Date = 0
g.each((d) => {
d.forEach((datum, index) => {
datum.times.forEach((time, j) => {
const startTime =
time.starting_time instanceof Date ? time.starting_time.getTime() : time.starting_time
const endTime =
time.ending_time instanceof Date ? time.ending_time.getTime() : time.ending_time
if (index === 0 && j === 0) {
originTime = startTime
time.starting_time = 0
time.ending_time = endTime - (originTime as number)
} else {
time.starting_time = startTime - (originTime as number)
time.ending_time = endTime - (originTime as number)
}
})
})
})
}
// Check how many stacks we're gonna need
// Do this here so that we can draw the axis before the graph
if (stacked || ending === 0 || beginning === 0) {
g.each((d) => {
d.forEach((datum, index) => {
// Create y mapping for stacked graph
if (stacked && !(index in yAxisMapping)) {
yAxisMapping[index] = maxStack
maxStack++
}
// Figure out beginning and ending times if they are unspecified
datum.times.forEach((time) => {
const startTime =
time.starting_time instanceof Date ? time.starting_time.getTime() : time.starting_time
const endTime =
time.ending_time instanceof Date ? time.ending_time.getTime() : time.ending_time
if (beginning === 0) {
if (startTime < (minTime as number) || (minTime === 0 && timeIsRelative === false)) {
minTime = startTime
}
}
if (ending === 0) {
if (endTime > (maxTime as number)) {
maxTime = endTime
}
}
})
})
})
if (ending === 0) {
ending = maxTime
}
if (beginning === 0) {
beginning = minTime
}
}
const w = width as number
const scaleFactor =
(1 / (toNumber(ending) - toNumber(beginning))) * (w - margin.left - margin.right)
// Draw the axis
const xScale = scaleTime()
.domain([toDate(beginning), toDate(ending)])
.range([margin.left, w - margin.right])
const xAxis = (orient === 'bottom' ? axisBottom(xScale) : axisTop(xScale)).tickSize(
tickFormat.tickSize || 6,
)
if (tickFormat.format) {
xAxis.tickFormat(tickFormat.format as any)
}
if (tickFormat.tickValues != null) {
xAxis.tickValues(tickFormat.tickValues)
} else if (tickFormat.tickTime && tickFormat.tickInterval) {
xAxis.ticks(tickFormat.tickTime, tickFormat.tickInterval)
} else if (tickFormat.numTicks) {
xAxis.ticks(tickFormat.numTicks)
}
// Helper functions for positioning
const getXPos = (d: TimelineTime): number => {
return margin.left + (toNumber(d.starting_time) - toNumber(beginning)) * scaleFactor
}
const getXTextPos = (d: TimelineTime): number => {
return margin.left + (toNumber(d.starting_time) - toNumber(beginning)) * scaleFactor + 5
}
const getStackPosition = (index: number): number => {
if (stacked) {
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index]
}
return margin.top
}
const getStackTextPosition = (index: number): number => {
if (stacked) {
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75
}
return margin.top + itemHeight * 0.75
}
// Draw the chart
g.each((d) => {
chartData = d
d.forEach((datum, index) => {
const data = datum.times
const hasLabel = typeof datum.label !== 'undefined'
// Issue warning about using id per data set. Ids should be individual to data elements
if (typeof datum.id !== 'undefined') {
console.warn(
"d3Timeline Warning: Ids per dataset is deprecated in favor of a 'class' key. Ids are now per data element.",
)
}
if (backgroundColor) {
appendBackgroundBar(yAxisMapping, index, g, data, datum)
}
g.selectAll('svg')
.data(data)
.enter()
.append((d) => {
return document.createElementNS(
'http://www.w3.org/2000/svg',
'display' in d ? (d.display as string) : display,
)
})
.attr('x', getXPos)
.attr('y', () => getStackPosition(index))
.attr('width', (d) => {
return (toNumber(d.ending_time) - toNumber(d.starting_time)) * scaleFactor
})
.attr('cy', () => getStackPosition(index) + itemHeight / 2)
.attr('cx', getXPos)
.attr('r', itemHeight / 2)
.attr('height', itemHeight)
.style('fill', (d) => {
if (d.color) return d.color
if (colorPropertyName) {
const dColorPropName = d[colorPropertyName]
if (dColorPropName) {
return colorCycle(dColorPropName)
} else {
return colorCycle(datum[colorPropertyName]!)
}
}
return colorCycle(index.toString())
})
.on('mousemove', (event, d) => {
hover(d, index, datum)
})
.on('mouseover', (event, d) => {
mouseover(d, index, datum)
})
.on('mouseout', (event, d) => {
mouseout(d, index, datum)
})
.on('click', (event, d) => {
click(d, index, datum)
})
.attr('class', () => {
return datum.class ? `timelineSeries_${datum.class}` : `timelineSeries_${index}`
})
.attr('id', (d, i) => {
// Use deprecated id field
if (datum.id && !d.id) {
return `timelineItem_${datum.id}`
}
return d.id ? d.id : `timelineItem_${index}_${i}`
})
g.selectAll('svg')
.data(data)
.enter()
.append('text')
.attr('x', getXTextPos)
.attr('y', () => getStackTextPosition(index))
.text((d) => d.label || '')
if (rowSeparatorsColor) {
const lineYAxis =
itemHeight +
itemMargin / 2 +
margin.top +
(itemHeight + itemMargin) * yAxisMapping[index]
gParent
.append('line')
.attr('class', 'row-separator')
.attr('x1', 0 + margin.left)
.attr('x2', w - margin.right)
.attr('y1', lineYAxis)
.attr('y2', lineYAxis)
.attr('stroke-width', 1)
.attr('stroke', rowSeparatorsColor)
}
// Add the label
if (hasLabel) {
appendLabel(gParent, yAxisMapping, index, hasLabel, datum)
}
if (typeof datum.icon !== 'undefined') {
gParent
.append('image')
.attr('class', 'timeline-label')
.attr(
'transform',
`translate(0, ${margin.top + (itemHeight + itemMargin) * yAxisMapping[index]})`,
)
.attr('xlink:href', datum.icon)
.attr('width', margin.left)
.attr('height', itemHeight)
}
})
})
const belowLastItem = margin.top + (itemHeight + itemMargin) * maxStack
const aboveFirstItem = margin.top
const timeAxisYPosition = showAxisTop ? aboveFirstItem : belowLastItem
if (showTimeAxis) {
appendTimeAxis(g, xAxis, timeAxisYPosition)
}
if (timeAxisTick) {
appendTimeAxisTick(g, xAxis, maxStack)
}
if (w > gParentSize.width) {
const move = (event: any) => {
const x = Math.min(0, Math.max(gParentSize.width - w, event.transform.x))
zoomBehavior.transform(gParent, zoomIdentity.translate(x, 0))
g.attr('transform', `translate(${x}, 0)`)
scroll(x * scaleFactor, xScale)
}
const zoomBehavior: ZoomBehavior<SVGSVGElement, any> = d3Zoom<SVGSVGElement, any>()
.scaleExtent([1, 1]) // Disable scaling, only allow panning
.on('zoom', move)
gParent.attr('class', 'scrollable').call(zoomBehavior)
}
if (rotateTicks) {
g.selectAll<SVGTextElement, unknown>('.tick text').attr('transform', function () {
const bbox = this.getBBox()
return `rotate(${rotateTicks})translate(${bbox.width / 2 + 10}, ${bbox.height / 2})`
})
}
const gNode = g.node()
const gSize = gNode ? gNode.getBoundingClientRect() : { height: 0, top: 0 }
setHeight()
if (showBorderLine) {
g.each((d) => {
d.forEach((datum) => {
const times = datum.times
times.forEach((time) => {
appendLine(xScale(toDate(time.starting_time))!, showBorderFormat)
appendLine(xScale(toDate(time.ending_time))!, showBorderFormat)
})
})
})
}
if (showTodayLine) {
const todayLine = xScale(new Date())
if (todayLine) appendLine(todayLine, showTodayFormat)
}
function setHeight() {
const h = height
if (!h && !gParentItem.attr('height')) {
if (itemHeight) {
// Set height based off of item height
height = gSize.height + gSize.top - gParentSize.top
// Set bounding rectangle height
select(gParentNode).attr('height', height)
} else {
throw new Error('height of the timeline is not set')
}
} else {
if (!h) {
height = parseInt(gParentItem.attr('height'))
} else {
gParentItem.attr('height', h)
}
}
}
function setWidth() {
const w = width
if (!w && !gParentSize.width) {
try {
const attrWidth = gParentItem.attr('width')
if (!attrWidth) {
throw new Error(
'width of the timeline is not set. As of Firefox 27, timeline().width(x) needs to be explicitly set in order to render',
)
}
width = parseInt(attrWidth)
} catch (err) {
console.log(err)
}
} else if (!(w && gParentSize.width)) {
try {
width = parseInt(gParentItem.attr('width'))
} catch (err) {
console.log(err)
}
}
// If both are set, do nothing
}
function appendLine(lineScale: number, lineFormat: TimelineLineFormat) {
const h = height as number
gParent
.append('line')
.attr('x1', lineScale)
.attr('y1', lineFormat.marginTop)
.attr('x2', lineScale)
.attr('y2', h - lineFormat.marginBottom)
.style(
'stroke',
typeof lineFormat.color === 'string'
? lineFormat.color
: lineFormat.color(lineScale.toString()),
)
.style('stroke-width', lineFormat.width)
}
}
// ============================================================================
// Utility Functions
// ============================================================================
function toNumber(value: number | Date): number {
return value instanceof Date ? value.getTime() : value
}
function toDate(value: number | Date): Date {
return value instanceof Date ? value : new Date(value)
}
// ============================================================================
// Builder Pattern Methods
// ============================================================================
const builder = timelineFunction as TimelineBuilder
builder.margin = function (p: TimelineMargin) {
margin = p
return builder
}
builder.orient = function (orientation: 'bottom' | 'top') {
orient = orientation
return builder
}
builder.itemHeight = function (h: number) {
itemHeight = h
return builder
}
builder.itemMargin = function (h: number) {
itemMargin = h
return builder
}
builder.navMargin = function (h: number) {
navMargin = h
return builder
}
builder.height = function (h: number) {
height = h
return builder
}
builder.width = function (w: number) {
width = w
return builder
}
builder.display = function (displayType: DisplayType) {
if (DISPLAY_TYPES.includes(displayType)) {
display = displayType
}
return builder
}
builder.labelFormat = function (f: (label: string) => string) {
labelFunction = f
return builder
}
builder.tickFormat = function (format: TimelineTickFormat) {
tickFormat = format
return builder
}
builder.hover = function (hoverFunc: TimelineCallback<void>) {
hover = hoverFunc
return builder
}
builder.mouseover = function (mouseoverFunc: TimelineCallback<void>) {
mouseover = mouseoverFunc
return builder
}
builder.mouseout = function (mouseoutFunc: TimelineCallback<void>) {
mouseout = mouseoutFunc
return builder
}
builder.click = function (clickFunc: TimelineCallback<void>) {
click = clickFunc
return builder
}
builder.scroll = function (scrollFunc: (x: number, scale: ScaleTime<number, number>) => void) {
scroll = scrollFunc
return builder
}
builder.colors = function (colorFormat: ScaleOrdinal<string, string> | string[]) {
if (Array.isArray(colorFormat)) {
colorCycle = scaleOrdinal(colorFormat)
} else {
colorCycle = colorFormat
}
return builder
}
builder.beginning = function (b: number | Date) {
beginning = b
return builder
}
builder.ending = function (e: number | Date) {
ending = e
return builder
}
builder.labelMargin = function (m: number) {
labelMargin = m
return builder
}
builder.rotateTicks = function (degrees: number) {
rotateTicks = degrees
return builder
}
builder.stack = function () {
stacked = !stacked
return builder
}
builder.relativeTime = function () {
timeIsRelative = !timeIsRelative
return builder
}
builder.showBorderLine = function () {
showBorderLine = !showBorderLine
return builder
}
builder.showBorderFormat = function (borderFormat: TimelineLineFormat) {
showBorderFormat = borderFormat
return builder
}
builder.showToday = function () {
showTodayLine = !showTodayLine
return builder
}
builder.showTodayFormat = function (todayFormat: TimelineLineFormat) {
showTodayFormat = todayFormat
return builder
}
builder.colorProperty = function (colorProp: string | null) {
colorPropertyName = colorProp
return builder
}
builder.rowSeparators = function (color: string | null) {
rowSeparatorsColor = color
return builder
}
builder.background = function (
color: string | null | ((datum: TimelineDatum, index: number) => string),
) {
backgroundColor = color
return builder
}
builder.showTimeAxis = function () {
showTimeAxis = !showTimeAxis
return builder
}
builder.showAxisTop = function () {
showAxisTop = !showAxisTop
return builder
}
builder.showAxisCalendarYear = function () {
showAxisCalendarYear = !showAxisCalendarYear
return builder
}
builder.showTimeAxisTick = function () {
timeAxisTick = !timeAxisTick
return builder
}
builder.fullLengthBackgrounds = function () {
fullLengthBackgrounds = !fullLengthBackgrounds
return builder
}
builder.showTimeAxisTickFormat = function (format: TimelineAxisTickFormat) {
timeAxisTickFormat = format
return builder
}
builder.showAxisHeaderBackground = function (bgColor?: string) {
showAxisHeaderBackground = !showAxisHeaderBackground
if (bgColor) {
axisBgColor = bgColor
}
return builder
}
builder.navigate = function (
navigateBackwards: (beginning: number | Date, chartData: TimelineData) => void,
navigateForwards: (ending: number | Date, chartData: TimelineData) => void,
) {
navigateLeft = navigateBackwards
navigateRight = navigateForwards
showAxisNav = !showAxisNav
return builder
}
return builder
}
// ============================================================================
// Module Exports & D3 Integration
// ============================================================================
export default timeline
// Augment d3 namespace by attaching timeline to it
export function extendD3(d3Instance: any): typeof d3 {
d3Instance.timeline = timeline
return d3Instance
}
// D3 with Timeline Extension
// Import this file to get d3 with timeline() method attached
import * as d3Base from 'd3'
import timeline, { extendD3 } from './d3-timeline'
import './d3-timeline-plugin.d.ts' // Load type augmentation
// Create extended d3 object with timeline
const d3 = extendD3(d3Base) as typeof d3Base & { timeline: typeof timeline }
export default d3
export { timeline }
export * from 'd3'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment