Skip to content

Commit

Permalink
Allow setting each weekday label visibility individually
Browse files Browse the repository at this point in the history
  • Loading branch information
grubersjoe committed Aug 30, 2024
1 parent 29a5026 commit 1d9b259
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 287 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-activity-calendar",
"version": "2.4.0",
"version": "2.5.0",
"description": "React component to display activity data in calendar",
"author": "Jonathan Gruber <[email protected]>",
"license": "MIT",
Expand Down
59 changes: 51 additions & 8 deletions src/component/ActivityCalendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Tooltip as MuiTooltip } from '@mui/material';
import LinkTo from '@storybook/addon-links/react';
import type { Meta, StoryObj } from '@storybook/react';
import { Highlight, themes as prismThemes } from 'prism-react-renderer';
import { type ForwardedRef, cloneElement, useMemo, useRef } from 'react';
import { type ForwardedRef, type ReactElement, cloneElement, useMemo, useRef } from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import 'react-tooltip/dist/react-tooltip.css';
import { useDarkMode } from 'storybook-dark-mode';
Expand Down Expand Up @@ -54,6 +54,9 @@ const meta: Meta<ForwardedRef<Props>> = {
ref: {
control: false,
},
showWeekdayLabels: {
control: 'boolean',
},
style: {
control: false,
},
Expand Down Expand Up @@ -220,21 +223,17 @@ export const DateRanges: Story = {
);

return (
<>
<Stack>
<ActivityCalendar
{...args}
data={dataLong}
labels={{
totalCount: '{{count}} activities in 2022 & 2023',
}}
/>
<br />
<br />
<ActivityCalendar {...args} data={dataMedium} />
<br />
<br />
<ActivityCalendar {...args} data={dataShort} />
</>
</Stack>
);
},
};
Expand Down Expand Up @@ -495,7 +494,30 @@ export const WeekdayLabels: Story = {
},
render: args => {
const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]);
return <ActivityCalendar {...args} data={data} />;
return (
<Stack>
<div>
<StackHeading code="true">Show every second weekday</StackHeading>
<ActivityCalendar {...args} data={data} />
</div>

<div>
<StackHeading code="['mon', 'fri']">Show specific days</StackHeading>
<ActivityCalendar {...args} data={data} showWeekdayLabels={['mon', 'fri']} />
</div>

<div>
<StackHeading code="['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']">
Show every day
</StackHeading>
<ActivityCalendar
{...args}
data={data}
showWeekdayLabels={['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']}
/>
</div>
</Stack>
);
},
parameters: {
docs: {
Expand Down Expand Up @@ -603,6 +625,27 @@ export const ContainerRef: Story = {
},
};

const Stack = ({ children }: { children: Array<ReactElement> }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 40 }}>{children}</div>
);

const StackHeading = ({ children, code }: { children: string; code?: string }) => (
<div
role="heading"
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 16,
fontSize: 14,
fontWeight: 'bolder',
}}
>
{children}
{code && <code style={{ fontSize: 12, fontWeight: 'normal' }}>{code}</code>}
</div>
);

const Source = ({
code,
isDarkMode,
Expand Down
143 changes: 71 additions & 72 deletions src/component/ActivityCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import chroma from 'chroma-js';
import type { Day as WeekDay } from 'date-fns';
import { getYear, parseISO } from 'date-fns';
import {
type CSSProperties,
Expand All @@ -19,20 +18,16 @@ import type {
Activity,
BlockElement,
Color,
DayIndex,
DayName,
EventHandlerMap,
Labels,
ReactEvent,
SVGRectEventHandler,
ThemeInput,
Week,
} from '../types';
import {
generateEmptyData,
getClassName,
getMonthLabels,
groupByWeeks,
maxWeekdayLabelLength,
} from '../utils/calendar';
import { generateEmptyData, getClassName, groupByWeeks, range } from '../utils/calendar';
import { getMonthLabels, initWeekdayLabels, maxWeekdayLabelWidth } from '../utils/label';
import { createTheme } from '../utils/theme';

export interface Props {
Expand Down Expand Up @@ -127,8 +122,10 @@ export interface Props {
renderColorLegend?: (block: BlockElement, level: number) => ReactElement;
/**
* Toggle to show weekday labels left to the calendar.
* Alternatively, pass a list of ISO 8601 weekday names to show.
* For example `['mon', 'wed', 'fri']`.
*/
showWeekdayLabels?: boolean;
showWeekdayLabels?: boolean | Array<DayName>;
/**
* Style object to pass to component container.
*/
Expand Down Expand Up @@ -164,7 +161,7 @@ export interface Props {
/**
* Index of day to be used as start of week. 0 represents Sunday.
*/
weekStart?: WeekDay;
weekStart?: DayIndex;
}

const ActivityCalendar = forwardRef<HTMLElement, Props>(
Expand Down Expand Up @@ -212,13 +209,13 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
const firstActivity = activities[0] as Activity;
const year = getYear(parseISO(firstActivity.date));
const weeks = groupByWeeks(activities, weekStart);
const firstWeek = weeks[0] as Week;

const labels = Object.assign({}, DEFAULT_LABELS, labelsProp);
const labelHeight = hideMonthLabels ? 0 : fontSize + LABEL_MARGIN;

const weekdayLabelOffset = showWeekdayLabels
? maxWeekdayLabelLength(firstWeek, weekStart, labels.weekdays, fontSize) + LABEL_MARGIN
const weekdayLabels = initWeekdayLabels(showWeekdayLabels, weekStart);
const weekdayLabelOffset = weekdayLabels.shouldShow
? maxWeekdayLabelWidth(labels.weekdays, weekdayLabels, fontSize) + LABEL_MARGIN
: undefined;

function getDimensions() {
Expand Down Expand Up @@ -323,74 +320,75 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
{!loading && !hideColorLegend && (
<div className={getClassName('legend-colors', styles.legendColors)}>
<span style={{ marginRight: '0.4em' }}>{labels.legend.less}</span>
{Array(maxLevel + 1)
.fill(undefined)
.map((_, level) => {
const block = (
<svg width={blockSize} height={blockSize} key={level}>
<rect
width={blockSize}
height={blockSize}
fill={colorScale[level]}
rx={blockRadius}
ry={blockRadius}
/>
</svg>
);

return renderColorLegend ? renderColorLegend(block, level) : block;
})}
{range(maxLevel + 1).map(level => {
const block = (
<svg width={blockSize} height={blockSize} key={level}>
<rect
width={blockSize}
height={blockSize}
fill={colorScale[level]}
rx={blockRadius}
ry={blockRadius}
/>
</svg>
);

return renderColorLegend ? renderColorLegend(block, level) : block;
})}
<span style={{ marginLeft: '0.4em' }}>{labels.legend.more}</span>
</div>
)}
</footer>
);
}

function renderLabels() {
if (!showWeekdayLabels && hideMonthLabels) {
function renderWeekdayLabels() {
if (!weekdayLabels.shouldShow) {
return null;
}

return (
<>
{showWeekdayLabels && weeks[0] && (
<g className={getClassName('legend-weekday')}>
{weeks[0].map((_, index) => {
if (index % 2 === 0) {
return null;
}

const dayIndex = (index + weekStart) % 7;

return (
<text
x={-LABEL_MARGIN}
y={labelHeight + (blockSize + blockMargin) * index + blockSize / 2}
dominantBaseline="central"
textAnchor="end"
key={index}
>
{labels.weekdays[dayIndex]}
</text>
);
})}
</g>
)}
{!hideMonthLabels && (
<g className={getClassName('legend-month')}>
{getMonthLabels(weeks, labels.months).map(({ label, weekIndex }) => (
<text
x={(blockSize + blockMargin) * weekIndex}
dominantBaseline="hanging"
key={weekIndex}
>
{label}
</text>
))}
</g>
)}
</>
<g className={getClassName('legend-weekday')}>
{range(7).map(index => {
const dayIndex = ((index + weekStart) % 7) as DayIndex;

if (!weekdayLabels.byDayIndex(dayIndex)) {
return null;
}

return (
<text
x={-LABEL_MARGIN}
y={labelHeight + (blockSize + blockMargin) * index + blockSize / 2}
dominantBaseline="central"
textAnchor="end"
key={index}
>
{labels.weekdays[dayIndex]}
</text>
);
})}
</g>
);
}

function renderMonthLabels() {
if (hideMonthLabels) {
return null;
}

return (
<g className={getClassName('legend-month')}>
{getMonthLabels(weeks, labels.months).map(({ label, weekIndex }) => (
<text
x={(blockSize + blockMargin) * weekIndex}
dominantBaseline="hanging"
key={weekIndex}
>
{label}
</text>
))}
</g>
);
}

Expand Down Expand Up @@ -422,7 +420,8 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
className={getClassName('calendar', styles.calendar)}
style={{ marginLeft: weekdayLabelOffset }}
>
{!loading && renderLabels()}
{!loading && renderWeekdayLabels()}
{!loading && renderMonthLabels()}
{renderCalendar()}
</svg>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export interface Activity {
}

export type Week = Array<Activity | undefined>;
export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Sunday, 1 = Monday etc.
export type DayName = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';

export type WeekdayLabels = {
byDayIndex: (index: DayIndex) => boolean;
shouldShow: boolean;
};

export type Labels = Partial<{
months: Array<string>;
Expand Down
Loading

0 comments on commit 1d9b259

Please sign in to comment.