timeline-panel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Timeline Panel
2
+
3
+ A flexible React component for displaying time-based tariff/schedule panels with dynamic grouping and legends.
4
+
5
+ ## Features
6
+
7
+ - 🕐 **Flexible Time Display** - Display time ranges with visual timeline
8
+ - 🎨 **Customizable Legends** - Support custom color schemes with fallback to built-in defaults
9
+ - 📊 **Dynamic Grouping** - Group data by any criteria using custom filter functions
10
+ - 🔧 **Dynamic Properties** - Support for arbitrary additional properties in data items
11
+ - ⚡ **Performance Optimized** - Uses React.memo for efficient re-rendering
12
+ - 🎯 **TypeScript Support** - Full TypeScript support with type safety
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install timeline-panel
18
+ ```
19
+
20
+ ## Basic Usage
21
+
22
+ ```tsx
23
+ import React, { useState } from 'react';
24
+ import TimelinePanel, { TariffItem } from 'timeline-panel';
25
+
26
+ const testData: TariffItem[] = [
27
+ {
28
+ start_time: '00:00',
29
+ end_time: '09:00',
30
+ name: 'summer',
31
+ tou_desc: 'work(A)'
32
+ },
33
+ {
34
+ start_time: '08:00',
35
+ end_time: '18:00',
36
+ name: 'rain',
37
+ tou_desc: 'sleep(B)'
38
+ }
39
+ ];
40
+
41
+ function App() {
42
+ const [errorMap, setErrorMap] = useState<TariffItem[]>([]);
43
+
44
+ const customLegend = {
45
+ 'work(A)': '#ff0000',
46
+ 'sleep(B)': '#ffbf00',
47
+ };
48
+
49
+ const groupConfig = {
50
+ summer: {
51
+ title: 'summer',
52
+ filterFn: (item: TariffItem) => item.name === 'summer'
53
+ },
54
+ rain: {
55
+ title: 'rain',
56
+ filterFn: (item: TariffItem) => item.name === 'rain'
57
+ }
58
+ };
59
+
60
+ return (
61
+ <TimelinePanel
62
+ testData={testData}
63
+ setErrorMap={setErrorMap}
64
+ legendData={customLegend}
65
+ groupConfig={groupConfig}
66
+ />
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## API Reference
72
+
73
+ ### TimelinePanel Props
74
+
75
+ | Property | Type | Default | Description |
76
+ |----------|------|---------|-------------|
77
+ | `testData` | `TariffItem[]` | **Required** - Array of time data items |
78
+ | `setErrorMap` | `React.Dispatch<React.SetStateAction<TariffItem[]>>` | **Required** - State setter for error items |
79
+ | `width` | `string \| number` | `'100%'` | Component width |
80
+ | `height` | `string \| number` | `'auto'` | Component height |
81
+ | `mt` | `string \| number` | `undefined` | Margin top |
82
+ | `legendData` | `Record<string, string>` | `undefined` | Custom legend colors |
83
+ | `groupConfig` | `Record<string, GroupConfig>` | `undefined` | Grouping configuration |
84
+
85
+ ### TariffItem Interface
86
+
87
+ ```typescript
88
+ interface TariffItem {
89
+ id?: number;
90
+ start_mn?: string; // "MM-DD"
91
+ end_mn?: string; // "MM-DD"
92
+ start_week?: number; // 1=Mon ... 7=Sun
93
+ end_week?: number; // 1=Mon ... 7=Sun
94
+ start_time?: string; // "HH:mm"
95
+ end_time?: string; // "HH:mm"
96
+ tou_desc?: string; // Time period description
97
+ tou_type?: string; // Short code
98
+ [key: string]: any; // Additional properties
99
+ }
100
+ ```
101
+
102
+ ### GroupConfig Interface
103
+
104
+ ```typescript
105
+ interface GroupConfig {
106
+ title: string; // Display title
107
+ filterFn: (item: TariffItem) => boolean; // Filter function
108
+ }
109
+ ```
110
+
111
+ ## Advanced Features
112
+
113
+ ### Custom Grouping
114
+
115
+ Group your data by any criteria:
116
+
117
+ ```tsx
118
+ const groupConfig = {
119
+ workingHours: {
120
+ title: 'Working Hours',
121
+ filterFn: (item) => item.start_time >= '09:00' && item.end_time <= '17:00'
122
+ },
123
+ weekends: {
124
+ title: 'Weekends',
125
+ filterFn: (item) => item.start_week >= 6
126
+ }
127
+ };
128
+ ```
129
+
130
+ ### Dynamic Properties
131
+
132
+ Add any custom properties to your data:
133
+
134
+ ```tsx
135
+ const testData = [
136
+ {
137
+ start_time: '09:00',
138
+ end_time: '17:00',
139
+ tou_desc: 'Work',
140
+ priority: 'high', // Custom property
141
+ department: 'IT', // Custom property
142
+ cost: 150.75 // Custom property
143
+ }
144
+ ];
145
+ ```
146
+
147
+ ### Built-in Legend Support
148
+
149
+ The component includes built-in legend items for common scenarios:
150
+
151
+ - `'空缺(V)'` - Empty slots (gray)
152
+ - `'重複(R)'` - Overlapping slots (black)
153
+
154
+ These will automatically display even if not provided in `legendData`.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,5 @@
1
+ import { FC } from 'react';
2
+ declare const Legend: FC<{
3
+ legend: Record<string, string>;
4
+ }>;
5
+ export default Legend;
package/dist/Legend.js ADDED
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LEGEND } from './utils';
3
+ const Legend = ({ legend }) => {
4
+ const mergedLegend = Object.assign(Object.assign({}, LEGEND), legend);
5
+ return (_jsx("div", { style: { marginTop: '15px', width: '150px', textAlign: 'center' }, children: _jsx("div", { style: { border: '1px solid #000' }, children: Object.entries(mergedLegend).map(([label, color]) => (_jsxs("div", { style: {
6
+ display: 'flex',
7
+ justifyContent: 'space-between',
8
+ alignItems: 'center',
9
+ border: '1px solid #000',
10
+ }, children: [_jsx("div", { style: {
11
+ borderRight: '2px solid #000',
12
+ width: '150px',
13
+ padding: '2px',
14
+ }, children: label }), _jsx("div", { style: { padding: '2px', width: '30px' }, children: _jsx("div", { style: {
15
+ width: '100%',
16
+ height: '10px',
17
+ border: '1px solid #000',
18
+ backgroundColor: color,
19
+ } }) })] }, label))) }) }));
20
+ };
21
+ export default Legend;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { TimelinePanelProps } from './types';
3
+ declare const _default: React.NamedExoticComponent<TimelinePanelProps>;
4
+ export default _default;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo, useEffect, useState } from 'react';
3
+ import { LEGEND, tou_descColor, splitTariffData, markOverlap, timeRangePercentage } from './utils';
4
+ import Legend from './Legend';
5
+ const TimelinePanel = ({ width = '100%', height = 'auto', mt, testData, setErrorMap, legendData, groupConfig }) => {
6
+ const [tariffGroups, setTariffGroups] = useState({});
7
+ useEffect(() => {
8
+ const groups = splitTariffData(testData, groupConfig);
9
+ const markedGroups = {};
10
+ const allErrorItems = [];
11
+ Object.entries(groups).forEach(([key, group]) => {
12
+ markedGroups[key] = {
13
+ title: group.title,
14
+ data: markOverlap(group.data)
15
+ };
16
+ const errorItems = markedGroups[key].data.filter((item) => item.tou_type === 'r' || item.tou_type === 'v');
17
+ allErrorItems.push(...errorItems);
18
+ });
19
+ setErrorMap(allErrorItems);
20
+ setTariffGroups(markedGroups);
21
+ }, [testData]);
22
+ return (_jsxs("div", { style: {
23
+ width,
24
+ height,
25
+ marginTop: mt,
26
+ padding: 16,
27
+ border: '1px solid #ddd',
28
+ borderRadius: 8,
29
+ display: 'flex',
30
+ flexDirection: 'row',
31
+ }, children: [_jsx("div", { style: { width: '90%' }, children: testData.length < 1 ? (_jsx("div", { style: { textAlign: 'center' }, children: "No Data" })) : (_jsx(_Fragment, { children: Object.values(tariffGroups).map((group) => {
32
+ const { title, data } = group;
33
+ if (!data.length)
34
+ return null;
35
+ return (_jsxs("div", { style: { display: 'flex', width: '90%', height: '60px', padding: '0 5%', marginBottom: '5px' }, children: [_jsx("h3", { style: { width: '20%', alignSelf: 'center' }, children: title }), data.map((item, index) => {
36
+ const isLast = index === data.length - 1;
37
+ const widthPercent = timeRangePercentage(item.start_time, item.end_time);
38
+ const bgColor = tou_descColor(item.tou_desc, legendData || LEGEND);
39
+ return (_jsxs("div", { style: {
40
+ width: `${widthPercent}%`,
41
+ display: 'flex',
42
+ justifyContent: 'center',
43
+ alignItems: 'center',
44
+ position: 'relative',
45
+ }, children: [_jsx("div", { style: { height: '50px', background: 'black', width: '5px', position: 'absolute', left: 0 } }), _jsx("div", { style: {
46
+ height: '10px',
47
+ background: 'white',
48
+ position: 'absolute',
49
+ top: 0,
50
+ left: '-18px',
51
+ lineHeight: '10px',
52
+ }, title: item.start_time || '', children: item.start_time || '' }), _jsx("div", { style: { height: '10px', width: '100%', background: bgColor, cursor: 'pointer' }, title: item.tou_desc || '' }), isLast && (_jsxs(_Fragment, { children: [_jsx("div", { style: { height: '50px', background: 'black', width: '5px' } }), _jsx("div", { style: {
53
+ height: '10px',
54
+ background: 'white',
55
+ position: 'absolute',
56
+ top: 0,
57
+ right: '-18px',
58
+ lineHeight: '10px',
59
+ }, children: "24:00" })] }))] }, `${index + (item.start_time || '') + (item.end_time || '')}`));
60
+ })] }, title));
61
+ }) })) }), _jsx(Legend, { legend: legendData || LEGEND })] }));
62
+ };
63
+ export default memo(TimelinePanel);
@@ -0,0 +1,4 @@
1
+ export { default as TimelinePanel } from './TimelinePanel';
2
+ export type { TariffItem, TimelinePanelProps, GroupConfig } from './types';
3
+ export { LEGEND, tou_descColor, splitTariffData, markOverlap, timeRangePercentage } from './utils';
4
+ export { default as Legend } from './Legend';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Main component
2
+ export { default as TimelinePanel } from './TimelinePanel';
3
+ // Utilities
4
+ export { LEGEND, tou_descColor, splitTariffData, markOverlap, timeRangePercentage } from './utils';
5
+ // Sub-components
6
+ export { default as Legend } from './Legend';
@@ -0,0 +1,26 @@
1
+ export interface TariffItem {
2
+ id?: number;
3
+ start_mn?: string;
4
+ end_mn?: string;
5
+ start_week?: number;
6
+ end_week?: number;
7
+ start_time?: string;
8
+ end_time?: string;
9
+ tou_desc?: string;
10
+ tou_type?: string;
11
+ [key: string]: any;
12
+ }
13
+ export type GroupConfig = {
14
+ title: string;
15
+ filterFn: (item: TariffItem) => boolean;
16
+ };
17
+ export type TimelinePanelProps = {
18
+ width?: string | number;
19
+ height?: string | number;
20
+ mt?: string | number;
21
+ testData: TariffItem[];
22
+ setErrorMap: React.Dispatch<React.SetStateAction<TariffItem[]>>;
23
+ legendData?: Record<string, string>;
24
+ groupConfig?: Record<string, GroupConfig>;
25
+ [key: string]: any;
26
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { TariffItem, GroupConfig } from './types';
2
+ export declare const LEGEND: Record<string, string>;
3
+ export declare const toMinutes: (time: string) => number;
4
+ export declare const timeRangePercentage: (start_time: string | undefined, end_time: string | undefined) => number;
5
+ export declare const tou_descColor: (tou_desc: string | undefined, legend: Record<string, string>) => string;
6
+ export declare const splitTariffData: (data: TariffItem[], groupConfig?: Record<string, GroupConfig>) => Record<string, {
7
+ title: string;
8
+ data: TariffItem[];
9
+ }>;
10
+ export declare const markOverlap: (tou_descs: TariffItem[]) => TariffItem[];
package/dist/utils.js ADDED
@@ -0,0 +1,93 @@
1
+ // Built-in legend colors
2
+ export const LEGEND = {
3
+ 'empty(V)': '#BBB',
4
+ 'overlap(R)': '#000',
5
+ };
6
+ // Time utilities
7
+ export const toMinutes = (time) => {
8
+ const [h, m] = time.split(':').map(Number);
9
+ return h * 60 + m;
10
+ };
11
+ export const timeRangePercentage = (start_time, end_time) => {
12
+ if (!start_time || !end_time)
13
+ return 0;
14
+ const startMinutes = toMinutes(start_time);
15
+ let endMinutes = toMinutes(end_time);
16
+ if (endMinutes <= startMinutes)
17
+ endMinutes += 24 * 60;
18
+ return ((endMinutes - startMinutes) / (24 * 60)) * 100;
19
+ };
20
+ // Color utilities
21
+ export const tou_descColor = (tou_desc, legend) => {
22
+ var _a, _b;
23
+ if (!tou_desc)
24
+ return 'gray';
25
+ return (_b = (_a = legend[tou_desc]) !== null && _a !== void 0 ? _a : LEGEND[tou_desc]) !== null && _b !== void 0 ? _b : 'gray';
26
+ };
27
+ // Data processing utilities
28
+ export const splitTariffData = (data, groupConfig) => {
29
+ const result = {};
30
+ if (groupConfig) {
31
+ Object.entries(groupConfig).forEach(([groupKey, config]) => {
32
+ const filteredData = data.filter(config.filterFn);
33
+ if (filteredData.length > 0) {
34
+ result[groupKey] = {
35
+ title: config.title,
36
+ data: filteredData
37
+ };
38
+ }
39
+ });
40
+ }
41
+ else {
42
+ const groups = {};
43
+ data.forEach((d) => {
44
+ const groupName = d.tou_desc || 'default';
45
+ if (!groups[groupName]) {
46
+ groups[groupName] = [];
47
+ }
48
+ groups[groupName].push(d);
49
+ });
50
+ Object.entries(groups).forEach(([groupName, groupData]) => {
51
+ result[groupName] = {
52
+ title: groupName,
53
+ data: groupData
54
+ };
55
+ });
56
+ }
57
+ return result;
58
+ };
59
+ export const markOverlap = (tou_descs) => {
60
+ var _a, _b, _c, _d;
61
+ if (!tou_descs.length)
62
+ return [];
63
+ const timePoints = new Set();
64
+ tou_descs.forEach((p) => {
65
+ if (p.start_time)
66
+ timePoints.add(p.start_time);
67
+ if (p.end_time)
68
+ timePoints.add(p.end_time);
69
+ });
70
+ const sortedTimes = Array.from(timePoints).sort((a, b) => toMinutes(a) - toMinutes(b));
71
+ const result = [];
72
+ for (let i = 0; i < sortedTimes.length - 1; i++) {
73
+ const segStart = sortedTimes[i];
74
+ const segEnd = sortedTimes[i + 1];
75
+ const covering = tou_descs.filter((p) => p.start_time && p.end_time && p.start_time < segEnd && p.end_time > segStart);
76
+ if (covering.length === 0) {
77
+ result.push(Object.assign(Object.assign({}, tou_descs[0]), { start_time: segStart, end_time: segEnd, tou_desc: 'empty(V)', tou_type: 'v' }));
78
+ }
79
+ else if (covering.length === 1) {
80
+ result.push(Object.assign(Object.assign({}, covering[0]), { start_time: segStart, end_time: segEnd }));
81
+ }
82
+ else {
83
+ result.push(Object.assign(Object.assign({}, covering[0]), { start_time: segStart, end_time: segEnd, tou_desc: 'overlap(R)', tou_type: 'r' }));
84
+ }
85
+ }
86
+ if (((_a = result[0]) === null || _a === void 0 ? void 0 : _a.start_time) !== '00:00') {
87
+ result.unshift(Object.assign(Object.assign({}, tou_descs[0]), { start_time: '00:00', end_time: ((_b = result[0]) === null || _b === void 0 ? void 0 : _b.start_time) || '00:00', tou_desc: 'empty(V)', tou_type: 'v' }));
88
+ }
89
+ if (((_c = result[result.length - 1]) === null || _c === void 0 ? void 0 : _c.end_time) !== '24:00') {
90
+ result.push(Object.assign(Object.assign({}, tou_descs[0]), { start_time: ((_d = result[result.length - 1]) === null || _d === void 0 ? void 0 : _d.end_time) || '24:00', end_time: '24:00', tou_desc: 'empty(V)', tou_type: 'v' }));
91
+ }
92
+ return result;
93
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "timeline-panel",
3
+ "version": "1.0.0",
4
+ "description": "A flexible React component for displaying time-based tariff/schedule panels",
5
+ "main": "dist/TimelinePanel.js",
6
+ "types": "dist/TimelinePanel.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch"
13
+ },
14
+ "keywords": [
15
+ "react",
16
+ "typescript",
17
+ "timeline",
18
+ "schedule",
19
+ "ui-component"
20
+ ],
21
+ "author": "Your Name",
22
+ "license": "MIT",
23
+ "peerDependencies": {
24
+ "react": ">=16.8.0",
25
+ "react-dom": ">=16.8.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^18.2.0",
29
+ "@types/react-dom": "^18.2.0",
30
+ "typescript": "^5.0.0"
31
+ }
32
+ }