Skip to content

Commit

Permalink
readable dates, durations, booleans on insights
Browse files Browse the repository at this point in the history
  • Loading branch information
xvvvyz committed Jun 30, 2024
1 parent 467ec00 commit df60e44
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 35 deletions.
92 changes: 76 additions & 16 deletions app/_components/plot-figure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import ChartType from '@/_constants/enum-chart-type';
import LineCurveFunction from '@/_constants/enum-line-curve-function';
import TimeSinceMilliseconds from '@/_constants/enum-time-since-milliseconds';
import { ListEventsData } from '@/_queries/list-events';
import formatDateTime from '@/_utilities/format-date-time';
import formatDirtyColumnHeader from '@/_utilities/format-dirty-column-header';
import formatTabularEvents from '@/_utilities/format-tabular-events';
import getInputDetailsFromEvents from '@/_utilities/get-input-details-from-events';
import humanizeDurationShort from '@/_utilities/humanize-duration-short';
import * as P from '@observablehq/plot';
import { useParentSize } from '@visx/responsive';
import { throttle } from 'lodash';
Expand Down Expand Up @@ -72,7 +74,7 @@ const PlotFigure = ({
useEffect(() => {
if (!containerRef.current) return;

const { input, isInputNominal } = getInputDetailsFromEvents({
const { input, isDuration, isInputNominal } = getInputDetailsFromEvents({
events,
inputId,
});
Expand All @@ -93,11 +95,20 @@ const PlotFigure = ({
const y = formatDirtyColumnHeader(input?.label);

marks.push(
P.axisX({ fill: '#C3C3C2', stroke: 'hsla(0, 0%, 100%, 10%)' }),
P.axisX({
fill: '#C3C3C2',
stroke: 'hsla(0, 0%, 100%, 10%)',
}),
);

marks.push(
P.axisY({ fill: '#C3C3C2', stroke: 'hsla(0, 0%, 100%, 10%)' }),
P.axisY({
fill: '#C3C3C2',
stroke: 'hsla(0, 0%, 100%, 10%)',
tickFormat: isDuration
? (t) => `${humanizeDurationShort(t * 1000)}`
: undefined,
}),
);

if (showBars) {
Expand Down Expand Up @@ -139,23 +150,12 @@ const PlotFigure = ({
}

if (!showBars || !isInputNominal) {
marks.push(
P[isInputNominal ? 'crosshairY' : 'crosshairX'](rows, {
maxRadius: 100,
ruleStroke: 'hsla(0, 0%, 100%, 25%)',
ruleStrokeOpacity: 1,
textFill: '#fff',
textStroke: '#1A1917',
textStrokeWidth: 10,
x,
y,
}),
);
const pointer = P[isInputNominal ? 'pointerY' : 'pointerX'];

marks.push(
P.dot(
rows,
P[isInputNominal ? 'pointerY' : 'pointerX']({
pointer({
fill: '#fff',
maxRadius: 100,
title: (d) => JSON.stringify(d),
Expand All @@ -164,6 +164,66 @@ const PlotFigure = ({
}),
),
);

marks.push(
P.ruleX(
rows,
pointer({
maxRadius: 100,
py: y,
stroke: 'hsla(0, 0%, 100%, 25%)',
x,
}),
),
);

marks.push(
P.ruleY(
rows,
pointer({
maxRadius: 100,
px: x,
stroke: 'hsla(0, 0%, 100%, 25%)',
y,
}),
),
);

marks.push(
P.text(
rows,
pointer({
dy: 16,
fill: '#fff',
frameAnchor: 'bottom',
maxRadius: 100,
stroke: '#1A1917',
strokeWidth: 10,
text: (d) => formatDateTime(d[x], { month: 'long' }),
textAnchor: 'middle',
x,
}),
),
);

marks.push(
P.text(
rows,
pointer({
dx: -9,
fill: '#fff',
frameAnchor: 'left',
maxRadius: 100,
px: x,
stroke: '#1A1917',
strokeWidth: 10,
text: (d) =>
isDuration ? `${humanizeDurationShort(d[y] * 1000)}` : d[y],
textAnchor: 'end',
y,
}),
),
);
}

if (syncDate) {
Expand Down
6 changes: 5 additions & 1 deletion app/_constants/constant-nominal-input-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import InputType from '@/_constants/enum-input-type';

const NOMINAL_INPUT_TYPES = [InputType.MultiSelect, InputType.Select];
const NOMINAL_INPUT_TYPES = [
InputType.Checkbox,
InputType.MultiSelect,
InputType.Select,
];

export default NOMINAL_INPUT_TYPES;
6 changes: 5 additions & 1 deletion app/_utilities/format-date-time.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const formatDateTime = (input: Date | string) =>
const formatDateTime = (
input: Date | string,
options?: Intl.DateTimeFormatOptions,
) =>
(input instanceof Date ? input : new Date(input)).toLocaleString(undefined, {
day: 'numeric',
hour: 'numeric',
hour12: true,
minute: 'numeric',
month: 'numeric',
...options,
});

export default formatDateTime;
25 changes: 8 additions & 17 deletions app/_utilities/format-input-value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import parseSeconds from './parse-seconds';
import humanizeDuration from 'humanize-duration';

interface Value {
label?: string;
Expand All @@ -7,29 +7,20 @@ interface Value {

const formatInputValue = {
checkbox: (values: Value[]) => (values[0]?.value == true ? 'Yes' : 'No'),
duration: (values: Value[]) => {
const value = values[0]?.value;
const t = parseSeconds(value as string);
let s = '';
if (t.hasHours) s += `${t.hours}:`;
return `${s}${t.minutes}:${t.seconds}`;
},
duration: (values: Value[]) =>
humanizeDuration(Number(values[0]?.value ?? '0') * 1000, { largest: 2 }),
multi_select: (values: Value[]) =>
values
.map(({ label }) => label)
.join(', ')
.replace(/, ([^,]+$)/, ', $1'),
number: (values: Value[]) => values[0]?.value,
select: (values: Value[]) => values[0]?.label,
stopwatch: (values: Value[]) => {
const t = parseSeconds(
(values.find(({ label }) => !label)?.value as string) ?? '0',
);

let s = '';
if (t.hasHours) s += `${t.hours}:`;
return `${s}${t.minutes}:${t.seconds}`;
},
stopwatch: (values: Value[]) =>
humanizeDuration(
Number(values.find(({ label }) => !label)?.value ?? '0') * 1000,
{ largest: 2 },
),
};

export default formatInputValue;
2 changes: 2 additions & 0 deletions app/_utilities/format-tabular-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const formatTabularEvents = (
if (options?.flattenInputs) flattenColumns.add(column);
} else if (input.input.type === InputType.Select) {
(row[column] as string) = strip(input.option?.label);
} else if (input.input.type === InputType.Checkbox) {
(row[column] as string) = input.value ? 'Yes' : 'No';
} else {
row[column] = Number(input.value);
}
Expand Down
1 change: 1 addition & 0 deletions app/_utilities/get-input-details-from-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const getInputDetailsFromEvents = ({

return {
input,
isDuration: input?.type === InputType.Duration,
isInputNominal: NOMINAL_INPUT_TYPES.includes(input?.type as InputType),
};
};
Expand Down
22 changes: 22 additions & 0 deletions app/_utilities/humanize-duration-short.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import humanizeDuration from 'humanize-duration';

const humanizeDurationShort = humanizeDuration.humanizer({
delimiter: ' ',
language: 'short',
languages: {
short: {
d: () => 'd',
h: () => 'h',
m: () => 'm',
mo: () => 'mo',
ms: () => 'ms',
s: () => 's',
w: () => 'w',
y: () => 'y',
},
},
largest: 2,
spacer: '',
});

export default humanizeDurationShort;
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/extension-youtube": "^2.4.0",
"@tiptap/react": "^2.4.0",
"@types/humanize-duration": "^3.27.4",
"@types/lodash": "^4.17.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand All @@ -56,6 +57,7 @@
"eslint-config-next": "^14.2.4",
"eslint-config-prettier": "^9.1.0",
"fuse.js": "^7.0.0",
"humanize-duration": "^3.32.1",
"json-2-csv": "^5.5.1",
"lodash": "^4.17.21",
"nanoid": "^5.0.7",
Expand Down

0 comments on commit df60e44

Please sign in to comment.