|
|
|
@ -11,50 +11,78 @@ interface TimelineProps { |
|
|
|
|
data: TimelineData[]; |
|
|
|
|
/** Width of the entire timeline, in pixels. */ |
|
|
|
|
width: number; |
|
|
|
|
/** Height of the entire timeline, in pixels. */ |
|
|
|
|
height: number; |
|
|
|
|
/** Whether the time is transformed to uppercase */ |
|
|
|
|
/** Whether the time is transformed to uppercase. */ |
|
|
|
|
isTimeUppercase?: boolean; |
|
|
|
|
/** Width of the middle timeline line, in pixels */ |
|
|
|
|
lineWidth?: number; |
|
|
|
|
/** Width of the outer circles on the timeline, in pixels. */ |
|
|
|
|
outerCircleWidth?: number; |
|
|
|
|
/** Width of the inner circles on the timeline, in pixels. */ |
|
|
|
|
innerCircleWidth?: number; |
|
|
|
|
/** Width of time label, in pixels. */ |
|
|
|
|
timeWidth?: number; |
|
|
|
|
/** Width of text label, in pixels. */ |
|
|
|
|
textWidth?: number; |
|
|
|
|
/** Distance between labels to middle line, in pixels. */ |
|
|
|
|
labelsOffset?: number; |
|
|
|
|
/** Distance between the time label AND the text label to middle line, in pixels. */ |
|
|
|
|
gap?: number; |
|
|
|
|
className?: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default function Timeline({ |
|
|
|
|
data, |
|
|
|
|
width, |
|
|
|
|
height, |
|
|
|
|
isTimeUppercase = true, |
|
|
|
|
lineWidth = 5, |
|
|
|
|
outerCircleWidth = 30, |
|
|
|
|
innerCircleWidth = 15, |
|
|
|
|
timeWidth = 200, |
|
|
|
|
textWidth = 300, |
|
|
|
|
labelsOffset = 50, |
|
|
|
|
gap = 50, |
|
|
|
|
className, |
|
|
|
|
}: TimelineProps) { |
|
|
|
|
const largerMiddleElemeent = |
|
|
|
|
outerCircleWidth > lineWidth ? outerCircleWidth : lineWidth; |
|
|
|
|
const requestedWidth = |
|
|
|
|
timeWidth + gap + largerMiddleElemeent + gap + textWidth; |
|
|
|
|
if (requestedWidth > width) { |
|
|
|
|
throw new Error( |
|
|
|
|
`<Timeline /> - timeWidth + gap + ${ |
|
|
|
|
outerCircleWidth > lineWidth ? "outerCircleWidth" : "lineWidth" |
|
|
|
|
} + gap + textWidth (${timeWidth} + ${gap} + ${largerMiddleElemeent} + ${gap} + ${textWidth} = ${requestedWidth}) is larger than width (${width})` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
if (innerCircleWidth > outerCircleWidth) { |
|
|
|
|
throw new Error( |
|
|
|
|
`<Timeline /> - innerCircleWidth (${innerCircleWidth}) is larger than outerCircleWidth (${outerCircleWidth})` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div |
|
|
|
|
className={ |
|
|
|
|
className ? `${className} ${styles.wrapper}` : `${styles.wrapper}` |
|
|
|
|
} |
|
|
|
|
style={{ width: width, height: height }} |
|
|
|
|
style={{ width: width }} |
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
className={styles.line} |
|
|
|
|
style={{ height: height, left: width / 2 }} |
|
|
|
|
></div> |
|
|
|
|
<div className={styles.timelineSections} style={{ width: width }}> |
|
|
|
|
style={{ |
|
|
|
|
width: lineWidth, |
|
|
|
|
left: width / 2 - lineWidth / 2, |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
<div className={styles.timelineSections}> |
|
|
|
|
{data.map((datum) => ( |
|
|
|
|
<TimelineSection |
|
|
|
|
key={datum.time} |
|
|
|
|
datum={datum} |
|
|
|
|
width={width} |
|
|
|
|
isTimeUppercase={isTimeUppercase} |
|
|
|
|
outerCircleWidth={outerCircleWidth} |
|
|
|
|
innerCircleWidth={innerCircleWidth} |
|
|
|
|
timeWidth={timeWidth} |
|
|
|
|
textWidth={textWidth} |
|
|
|
|
labelsOffset={labelsOffset} |
|
|
|
|
gap={gap} |
|
|
|
|
/> |
|
|
|
|
))} |
|
|
|
|
</div> |
|
|
|
@ -66,74 +94,73 @@ interface TimelineSectionProps { |
|
|
|
|
datum: TimelineData; |
|
|
|
|
width: number; |
|
|
|
|
isTimeUppercase: boolean; |
|
|
|
|
outerCircleWidth: number; |
|
|
|
|
innerCircleWidth: number; |
|
|
|
|
timeWidth: number; |
|
|
|
|
textWidth: number; |
|
|
|
|
labelsOffset: number; |
|
|
|
|
gap: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function TimelineSection({ |
|
|
|
|
datum, |
|
|
|
|
width, |
|
|
|
|
isTimeUppercase, |
|
|
|
|
outerCircleWidth, |
|
|
|
|
innerCircleWidth, |
|
|
|
|
timeWidth, |
|
|
|
|
|
|
|
|
|
textWidth, |
|
|
|
|
labelsOffset, |
|
|
|
|
gap, |
|
|
|
|
}: TimelineSectionProps) { |
|
|
|
|
const [onHover, setHover] = useState(false); |
|
|
|
|
|
|
|
|
|
const handleMouseEnter = () => { |
|
|
|
|
setHover(true); |
|
|
|
|
console.log(onHover); |
|
|
|
|
}; |
|
|
|
|
const handleMouseLeave = () => setHover(false); |
|
|
|
|
|
|
|
|
|
// divs for customizable margins are necessary, absolute positioning loses vertical flex-box functionality
|
|
|
|
|
return ( |
|
|
|
|
<div className={styles.timelineSection}> |
|
|
|
|
<div |
|
|
|
|
style={{ |
|
|
|
|
width: (width - labelsOffset - labelsOffset - 30) / 2 - timeWidth, |
|
|
|
|
}} |
|
|
|
|
></div> |
|
|
|
|
<div className={styles.timelineSection} style={{ gap: gap }}> |
|
|
|
|
<div |
|
|
|
|
className={onHover ? `${styles.time} ${styles.timeHover}` : styles.time} |
|
|
|
|
style={{ |
|
|
|
|
width: timeWidth, |
|
|
|
|
marginLeft: (width - 2 * gap - outerCircleWidth) / 2 - timeWidth, |
|
|
|
|
}} |
|
|
|
|
onMouseEnter={handleMouseEnter} |
|
|
|
|
onMouseLeave={handleMouseLeave} |
|
|
|
|
> |
|
|
|
|
{isTimeUppercase ? datum.time.toUpperCase() : datum.time} |
|
|
|
|
</div> |
|
|
|
|
<div style={{ width: labelsOffset }}></div> |
|
|
|
|
<div |
|
|
|
|
className={styles.circle} |
|
|
|
|
style={{ width: 30 }} |
|
|
|
|
onMouseEnter={handleMouseEnter} |
|
|
|
|
onMouseLeave={handleMouseLeave} |
|
|
|
|
style={{ |
|
|
|
|
width: outerCircleWidth, |
|
|
|
|
height: outerCircleWidth, |
|
|
|
|
borderRadius: outerCircleWidth, |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
className={styles.innerCircle} |
|
|
|
|
style={{ display: onHover ? "inline" : "none" }} |
|
|
|
|
></div> |
|
|
|
|
style={{ |
|
|
|
|
width: innerCircleWidth, |
|
|
|
|
height: innerCircleWidth, |
|
|
|
|
borderRadius: innerCircleWidth, |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
<div style={{ width: labelsOffset }}></div> |
|
|
|
|
<div |
|
|
|
|
className={onHover ? `${styles.text} ${styles.textHover}` : styles.text} |
|
|
|
|
style={{ |
|
|
|
|
width: textWidth, |
|
|
|
|
marginRight: (width - 2 * gap - outerCircleWidth) / 2 - textWidth, |
|
|
|
|
}} |
|
|
|
|
onMouseEnter={handleMouseEnter} |
|
|
|
|
onMouseLeave={handleMouseLeave} |
|
|
|
|
> |
|
|
|
|
{datum.text} |
|
|
|
|
</div> |
|
|
|
|
<div |
|
|
|
|
style={{ |
|
|
|
|
width: (width - labelsOffset - labelsOffset - 30) / 2 - textWidth, |
|
|
|
|
}} |
|
|
|
|
></div> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|