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

ui-spacetimechart: upgrades time captions #945

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
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
20 changes: 11 additions & 9 deletions ui-spacetimechart/src/components/PathLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useCallback } from 'react';

import { inRange, last } from 'lodash';

import { CAPTION_SIZE } from './TimeCaptions';
import { useDraw, usePicking } from '../hooks/useCanvas';
import {
type DataPoint,
Expand Down Expand Up @@ -199,6 +198,7 @@ export const PathLayer = ({
width,
height,
swapAxis,
captionSize,
theme: {
background,
pathsStyles: { fontSize, fontFamily },
Expand All @@ -211,10 +211,12 @@ export const PathLayer = ({
) => {
if (!label) return;

const firstPointOnScreenIndex = points.findIndex(({ x, y }) =>
!swapAxis
? inRange(x, 0, width) && inRange(y, 0, height - CAPTION_SIZE)
: inRange(x, CAPTION_SIZE, width) && inRange(y, 0, height)
const minX = swapAxis ? captionSize : 0;
const minY = 0;
const maxX = width;
const maxY = swapAxis ? height : height - captionSize;
const firstPointOnScreenIndex = points.findIndex(
({ x, y }) => inRange(x, minX, maxX) && inRange(y, minY, maxY)
);

if (firstPointOnScreenIndex < 0) return;
Expand All @@ -230,11 +232,11 @@ export const PathLayer = ({
if (next) angle = Math.atan2(next.y - curr.y, next.x - curr.x);
} else {
const slope = (curr.y - prev.y) / (curr.x - prev.x);
const yOnYAxisIntersect = curr.y - curr.x * slope;
const xOnXAxisIntersect = curr.x - curr.y / slope;
if (yOnYAxisIntersect >= 0) {
const yOnYAxisIntersect = curr.y - minY - (curr.x - minX) * slope;
const xOnXAxisIntersect = curr.x - minX - (curr.y - minY) / slope;
if (yOnYAxisIntersect >= minY) {
position = {
x: 0,
x: minX,
y: yOnYAxisIntersect,
};
} else {
Expand Down
7 changes: 7 additions & 0 deletions ui-spacetimechart/src/components/SpaceTimeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => {
hideGrid,
hidePathsLabels,
showTicks,
hideDates,
theme,
/* eslint-disable @typescript-eslint/no-unused-vars */
onPan,
Expand Down Expand Up @@ -76,6 +77,7 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => {
hideGrid,
hidePathsLabels,
showTicks,
hideDates,
}),
[
operationalPoints,
Expand All @@ -91,6 +93,7 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => {
hideGrid,
hidePathsLabels,
showTicks,
hideDates,
]
);

Expand Down Expand Up @@ -154,7 +157,11 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => {
hideGrid: !!hideGrid,
hidePathsLabels: !!hidePathsLabels,
showTicks: !!showTicks,
hideDates: !!hideDates,
theme: fullTheme,
captionSize: hideDates
? fullTheme.timeCaptionsSize
: fullTheme.dateCaptionsSize + fullTheme.timeCaptionsSize,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fingerprint]);
Expand Down
115 changes: 75 additions & 40 deletions ui-spacetimechart/src/components/TimeCaptions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useCallback } from 'react';

import { map } from 'lodash';

import { useDraw } from '../hooks/useCanvas';
import { MINUTE } from '../lib/consts';
import { HOUR, MINUTE } from '../lib/consts';
import { type DrawingFunction } from '../lib/types';
import { computeVisibleTimeMarkers, getCrispLineCoordinate } from '../utils/canvas';

const MARGIN = 100;
const MINUTES_FORMATTER = (t: number) => `:${new Date(t).getMinutes().toString().padStart(2, '0')}`;
const HOURS_FORMATTER = (t: number, pixelsPerMinute: number) => {
const date = new Date(t);
Expand All @@ -16,8 +19,15 @@ const HOURS_FORMATTER = (t: number, pixelsPerMinute: number) => {
return date.getHours().toString().padStart(2, '0');
}
};
const DATES_FORMATER = (t: number) => {
const date = new Date(t);
return [
date.getDate().toString().padStart(2, '0'),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getFullYear().toString(),
].join('/');
};

export const CAPTION_SIZE = 33;
const RANGES_FORMATER: ((t: number, pixelsPerMinute: number) => string)[] = [
() => '',
() => '',
Expand Down Expand Up @@ -51,14 +61,19 @@ const TimeCaptions = () => {
timeCaptionsPriorities,
timeCaptionsStyles,
timeGraduationsStyles,
dateCaptionsStyle,
},
captionSize,
hideDates,
showTicks,
}
) => {
const timeAxisSize = !swapAxis ? width : height;
const spaceAxisSize = !swapAxis ? height : width;
const minT = timeOrigin - timeScale * timePixelOffset;
const maxT = minT + timeScale * width;

// Add some margin, so that captions of times right outside the stage are still visible:
const minT = timeOrigin - timeScale * (timePixelOffset + MARGIN);
const maxT = minT + timeScale * (width + MARGIN * 2);

// Find which styles to apply, relatively to the timescale (i.e. horizontal zoom level):
const pixelsPerMinute = (1 / timeScale) * MINUTE;
Expand All @@ -72,71 +87,91 @@ const TimeCaptions = () => {
return false;
});

const labelMarkFormatter = (labelLevel: number, i: number) => ({
level: labelLevel,
rangeIndex: i,
});
const labelMarks = computeVisibleTimeMarkers(
minT,
maxT,
timeRanges,
labelLevels,
labelMarkFormatter
(level: number, i: number) => ({
level,
styles: timeCaptionsStyles[level],
formatter: RANGES_FORMATER[i],
})
);

let allMarksArray = map(labelMarks, (mark, t) => ({
...mark,
time: +t,
}));
if (!hideDates)
allMarksArray = allMarksArray.concat(
map(
computeVisibleTimeMarkers(minT, maxT, [24 * HOUR], [1], (level: number) => ({
level,
styles: dateCaptionsStyle,
formatter: DATES_FORMATER,
})),
(mark, t) => ({
...mark,
time: +t,
})
)
);
// Render caption background:
ctx.fillStyle = background;
if (!swapAxis) {
ctx.fillRect(0, spaceAxisSize - CAPTION_SIZE, timeAxisSize, CAPTION_SIZE);
ctx.fillRect(0, spaceAxisSize - captionSize, timeAxisSize, captionSize);
} else {
ctx.fillRect(0, 0, CAPTION_SIZE, timeAxisSize);
}

// Render caption top border:
ctx.strokeStyle = timeGraduationsStyles[1].color;
ctx.lineWidth = timeGraduationsStyles[1].width;
if (!showTicks) {
ctx.beginPath();
if (!swapAxis) {
const y = getCrispLineCoordinate(spaceAxisSize - CAPTION_SIZE, ctx.lineWidth);
ctx.moveTo(0, y);
ctx.lineTo(timeAxisSize, y);
} else {
const x = getCrispLineCoordinate(CAPTION_SIZE, ctx.lineWidth);
ctx.moveTo(x, 0);
ctx.lineTo(x, timeAxisSize);
}
ctx.stroke();
ctx.fillRect(0, 0, captionSize, timeAxisSize);
}

// Render time captions:
for (const t in labelMarks) {
const { level, rangeIndex } = labelMarks[t];
const styles = timeCaptionsStyles[level];
const formatter = RANGES_FORMATER[rangeIndex];
const text = formatter(+t, pixelsPerMinute);
allMarksArray.forEach(({ styles, formatter, time }) => {
const text = formatter(time, pixelsPerMinute);

ctx.textAlign = 'center';
ctx.textAlign = styles.textAlign || 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = styles.color;
ctx.lineWidth = 5;
ctx.strokeStyle = background;
ctx.lineCap = 'butt';
ctx.font = `${styles.fontWeight || 'normal'} ${styles.font}`;
const timePixel = getCrispLineCoordinate(getTimePixel(+t), ctx.lineWidth);
const timePixel = getCrispLineCoordinate(getTimePixel(time), ctx.lineWidth);

if (!swapAxis) {
if (showTicks) {
ctx.strokeStyle = timeCaptionsStyles[1].color;
ctx.moveTo(timePixel, spaceAxisSize - CAPTION_SIZE);
ctx.lineTo(timePixel, +t % 180000 === 0 ? 8 : 4);
ctx.moveTo(timePixel, spaceAxisSize - captionSize);
ctx.lineTo(timePixel, time % 180000 === 0 ? 8 : 4);
ctx.stroke();
}
ctx.fillText(text, timePixel, spaceAxisSize - CAPTION_SIZE + (styles.topOffset || 0));

ctx.strokeText(text, timePixel, spaceAxisSize - captionSize + (styles.topOffset || 0));
ctx.fillText(text, timePixel, spaceAxisSize - captionSize + (styles.topOffset || 0));
} else {
ctx.save();
ctx.translate(CAPTION_SIZE - (styles.topOffset || 0), timePixel);
ctx.translate(captionSize - (styles.topOffset || 0), timePixel);
ctx.rotate(Math.PI / 2);
ctx.strokeText(text, 0, 0);
ctx.fillText(text, 0, 0);
ctx.restore();
}
});

// Render caption top border:
ctx.strokeStyle = timeGraduationsStyles[1].color;
ctx.lineWidth = timeGraduationsStyles[1].width;
if (!showTicks) {
ctx.beginPath();
if (!swapAxis) {
const y = getCrispLineCoordinate(spaceAxisSize - captionSize, ctx.lineWidth);
ctx.moveTo(0, y);
ctx.lineTo(timeAxisSize, y);
} else {
const x = getCrispLineCoordinate(captionSize, ctx.lineWidth);
ctx.moveTo(x, 0);
ctx.lineTo(x, timeAxisSize);
}
ctx.stroke();
}
},
[]
Expand Down
Loading