Skip to content

Commit 1d9b259

Browse files
committed
Allow setting each weekday label visibility individually
1 parent 29a5026 commit 1d9b259

9 files changed

+462
-287
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-activity-calendar",
3-
"version": "2.4.0",
3+
"version": "2.5.0",
44
"description": "React component to display activity data in calendar",
55
"author": "Jonathan Gruber <gruberjonathan@gmail.com>",
66
"license": "MIT",

src/component/ActivityCalendar.stories.tsx

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Tooltip as MuiTooltip } from '@mui/material';
22
import LinkTo from '@storybook/addon-links/react';
33
import type { Meta, StoryObj } from '@storybook/react';
44
import { Highlight, themes as prismThemes } from 'prism-react-renderer';
5-
import { type ForwardedRef, cloneElement, useMemo, useRef } from 'react';
5+
import { type ForwardedRef, type ReactElement, cloneElement, useMemo, useRef } from 'react';
66
import { Tooltip as ReactTooltip } from 'react-tooltip';
77
import 'react-tooltip/dist/react-tooltip.css';
88
import { useDarkMode } from 'storybook-dark-mode';
@@ -54,6 +54,9 @@ const meta: Meta<ForwardedRef<Props>> = {
5454
ref: {
5555
control: false,
5656
},
57+
showWeekdayLabels: {
58+
control: 'boolean',
59+
},
5760
style: {
5861
control: false,
5962
},
@@ -220,21 +223,17 @@ export const DateRanges: Story = {
220223
);
221224

222225
return (
223-
<>
226+
<Stack>
224227
<ActivityCalendar
225228
{...args}
226229
data={dataLong}
227230
labels={{
228231
totalCount: '{{count}} activities in 2022 & 2023',
229232
}}
230233
/>
231-
<br />
232-
<br />
233234
<ActivityCalendar {...args} data={dataMedium} />
234-
<br />
235-
<br />
236235
<ActivityCalendar {...args} data={dataShort} />
237-
</>
236+
</Stack>
238237
);
239238
},
240239
};
@@ -495,7 +494,30 @@ export const WeekdayLabels: Story = {
495494
},
496495
render: args => {
497496
const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]);
498-
return <ActivityCalendar {...args} data={data} />;
497+
return (
498+
<Stack>
499+
<div>
500+
<StackHeading code="true">Show every second weekday</StackHeading>
501+
<ActivityCalendar {...args} data={data} />
502+
</div>
503+
504+
<div>
505+
<StackHeading code="['mon', 'fri']">Show specific days</StackHeading>
506+
<ActivityCalendar {...args} data={data} showWeekdayLabels={['mon', 'fri']} />
507+
</div>
508+
509+
<div>
510+
<StackHeading code="['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']">
511+
Show every day
512+
</StackHeading>
513+
<ActivityCalendar
514+
{...args}
515+
data={data}
516+
showWeekdayLabels={['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']}
517+
/>
518+
</div>
519+
</Stack>
520+
);
499521
},
500522
parameters: {
501523
docs: {
@@ -603,6 +625,27 @@ export const ContainerRef: Story = {
603625
},
604626
};
605627

628+
const Stack = ({ children }: { children: Array<ReactElement> }) => (
629+
<div style={{ display: 'flex', flexDirection: 'column', gap: 40 }}>{children}</div>
630+
);
631+
632+
const StackHeading = ({ children, code }: { children: string; code?: string }) => (
633+
<div
634+
role="heading"
635+
style={{
636+
display: 'flex',
637+
alignItems: 'center',
638+
gap: 12,
639+
marginBottom: 16,
640+
fontSize: 14,
641+
fontWeight: 'bolder',
642+
}}
643+
>
644+
{children}
645+
{code && <code style={{ fontSize: 12, fontWeight: 'normal' }}>{code}</code>}
646+
</div>
647+
);
648+
606649
const Source = ({
607650
code,
608651
isDarkMode,

src/component/ActivityCalendar.tsx

Lines changed: 71 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client';
22

33
import chroma from 'chroma-js';
4-
import type { Day as WeekDay } from 'date-fns';
54
import { getYear, parseISO } from 'date-fns';
65
import {
76
type CSSProperties,
@@ -19,20 +18,16 @@ import type {
1918
Activity,
2019
BlockElement,
2120
Color,
21+
DayIndex,
22+
DayName,
2223
EventHandlerMap,
2324
Labels,
2425
ReactEvent,
2526
SVGRectEventHandler,
2627
ThemeInput,
27-
Week,
2828
} from '../types';
29-
import {
30-
generateEmptyData,
31-
getClassName,
32-
getMonthLabels,
33-
groupByWeeks,
34-
maxWeekdayLabelLength,
35-
} from '../utils/calendar';
29+
import { generateEmptyData, getClassName, groupByWeeks, range } from '../utils/calendar';
30+
import { getMonthLabels, initWeekdayLabels, maxWeekdayLabelWidth } from '../utils/label';
3631
import { createTheme } from '../utils/theme';
3732

3833
export interface Props {
@@ -127,8 +122,10 @@ export interface Props {
127122
renderColorLegend?: (block: BlockElement, level: number) => ReactElement;
128123
/**
129124
* Toggle to show weekday labels left to the calendar.
125+
* Alternatively, pass a list of ISO 8601 weekday names to show.
126+
* For example `['mon', 'wed', 'fri']`.
130127
*/
131-
showWeekdayLabels?: boolean;
128+
showWeekdayLabels?: boolean | Array<DayName>;
132129
/**
133130
* Style object to pass to component container.
134131
*/
@@ -164,7 +161,7 @@ export interface Props {
164161
/**
165162
* Index of day to be used as start of week. 0 represents Sunday.
166163
*/
167-
weekStart?: WeekDay;
164+
weekStart?: DayIndex;
168165
}
169166

170167
const ActivityCalendar = forwardRef<HTMLElement, Props>(
@@ -212,13 +209,13 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
212209
const firstActivity = activities[0] as Activity;
213210
const year = getYear(parseISO(firstActivity.date));
214211
const weeks = groupByWeeks(activities, weekStart);
215-
const firstWeek = weeks[0] as Week;
216212

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

220-
const weekdayLabelOffset = showWeekdayLabels
221-
? maxWeekdayLabelLength(firstWeek, weekStart, labels.weekdays, fontSize) + LABEL_MARGIN
216+
const weekdayLabels = initWeekdayLabels(showWeekdayLabels, weekStart);
217+
const weekdayLabelOffset = weekdayLabels.shouldShow
218+
? maxWeekdayLabelWidth(labels.weekdays, weekdayLabels, fontSize) + LABEL_MARGIN
222219
: undefined;
223220

224221
function getDimensions() {
@@ -323,74 +320,75 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
323320
{!loading && !hideColorLegend && (
324321
<div className={getClassName('legend-colors', styles.legendColors)}>
325322
<span style={{ marginRight: '0.4em' }}>{labels.legend.less}</span>
326-
{Array(maxLevel + 1)
327-
.fill(undefined)
328-
.map((_, level) => {
329-
const block = (
330-
<svg width={blockSize} height={blockSize} key={level}>
331-
<rect
332-
width={blockSize}
333-
height={blockSize}
334-
fill={colorScale[level]}
335-
rx={blockRadius}
336-
ry={blockRadius}
337-
/>
338-
</svg>
339-
);
340-
341-
return renderColorLegend ? renderColorLegend(block, level) : block;
342-
})}
323+
{range(maxLevel + 1).map(level => {
324+
const block = (
325+
<svg width={blockSize} height={blockSize} key={level}>
326+
<rect
327+
width={blockSize}
328+
height={blockSize}
329+
fill={colorScale[level]}
330+
rx={blockRadius}
331+
ry={blockRadius}
332+
/>
333+
</svg>
334+
);
335+
336+
return renderColorLegend ? renderColorLegend(block, level) : block;
337+
})}
343338
<span style={{ marginLeft: '0.4em' }}>{labels.legend.more}</span>
344339
</div>
345340
)}
346341
</footer>
347342
);
348343
}
349344

350-
function renderLabels() {
351-
if (!showWeekdayLabels && hideMonthLabels) {
345+
function renderWeekdayLabels() {
346+
if (!weekdayLabels.shouldShow) {
352347
return null;
353348
}
354349

355350
return (
356-
<>
357-
{showWeekdayLabels && weeks[0] && (
358-
<g className={getClassName('legend-weekday')}>
359-
{weeks[0].map((_, index) => {
360-
if (index % 2 === 0) {
361-
return null;
362-
}
363-
364-
const dayIndex = (index + weekStart) % 7;
365-
366-
return (
367-
<text
368-
x={-LABEL_MARGIN}
369-
y={labelHeight + (blockSize + blockMargin) * index + blockSize / 2}
370-
dominantBaseline="central"
371-
textAnchor="end"
372-
key={index}
373-
>
374-
{labels.weekdays[dayIndex]}
375-
</text>
376-
);
377-
})}
378-
</g>
379-
)}
380-
{!hideMonthLabels && (
381-
<g className={getClassName('legend-month')}>
382-
{getMonthLabels(weeks, labels.months).map(({ label, weekIndex }) => (
383-
<text
384-
x={(blockSize + blockMargin) * weekIndex}
385-
dominantBaseline="hanging"
386-
key={weekIndex}
387-
>
388-
{label}
389-
</text>
390-
))}
391-
</g>
392-
)}
393-
</>
351+
<g className={getClassName('legend-weekday')}>
352+
{range(7).map(index => {
353+
const dayIndex = ((index + weekStart) % 7) as DayIndex;
354+
355+
if (!weekdayLabels.byDayIndex(dayIndex)) {
356+
return null;
357+
}
358+
359+
return (
360+
<text
361+
x={-LABEL_MARGIN}
362+
y={labelHeight + (blockSize + blockMargin) * index + blockSize / 2}
363+
dominantBaseline="central"
364+
textAnchor="end"
365+
key={index}
366+
>
367+
{labels.weekdays[dayIndex]}
368+
</text>
369+
);
370+
})}
371+
</g>
372+
);
373+
}
374+
375+
function renderMonthLabels() {
376+
if (hideMonthLabels) {
377+
return null;
378+
}
379+
380+
return (
381+
<g className={getClassName('legend-month')}>
382+
{getMonthLabels(weeks, labels.months).map(({ label, weekIndex }) => (
383+
<text
384+
x={(blockSize + blockMargin) * weekIndex}
385+
dominantBaseline="hanging"
386+
key={weekIndex}
387+
>
388+
{label}
389+
</text>
390+
))}
391+
</g>
394392
);
395393
}
396394

@@ -422,7 +420,8 @@ const ActivityCalendar = forwardRef<HTMLElement, Props>(
422420
className={getClassName('calendar', styles.calendar)}
423421
style={{ marginLeft: weekdayLabelOffset }}
424422
>
425-
{!loading && renderLabels()}
423+
{!loading && renderWeekdayLabels()}
424+
{!loading && renderMonthLabels()}
426425
{renderCalendar()}
427426
</svg>
428427
</div>

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export interface Activity {
1313
}
1414

1515
export type Week = Array<Activity | undefined>;
16+
export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Sunday, 1 = Monday etc.
17+
export type DayName = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';
18+
19+
export type WeekdayLabels = {
20+
byDayIndex: (index: DayIndex) => boolean;
21+
shouldShow: boolean;
22+
};
1623

1724
export type Labels = Partial<{
1825
months: Array<string>;

0 commit comments

Comments
 (0)