React 实现甘特图

在网上没有找到免费合适的甘特图,自己参考一些甘特图的范例开发了一个甘特图。使用场景,会议室使用情况预览

1.24小时制,根据开始时间和结束时间计算间隔宽度,使用开始时间计算开始位置
2.通过左边数据滚动条控制右边数据同步滚动;通过下边数据滚动条控制时间轴滚动
3.通过鼠标事件实时计算tooltip 位置,通过transform样式属性阻止tooltip溢出页面
4.无引用库 ,直接react 加 less实现

实现效果:


import React, { useState, useEffect, useRef } from 'react';
import styles from './style.less';

const GunttChart = (props) => {
    const {
        rowHeight = 40,  ///数据行高
        hourWidth = 80,  ///数据行宽
        startTime = 7,    ///默认起始时间     小屏幕会生效
        width = '100%',   
        bodyHeight = 'calc(100% - 40px)',
        rowTitle = '会议厅',     首列名称
        rows = [{                     ///行名
            title: '101会议厅',
            key: 101,
        }, {
            title: '102会议厅',
            key: 102,
        }, {
            title: '103会议厅',
            key: 103,
        }, {
            title: '104会议厅',
            key: 104,
        }, {
            title: '105会议厅',
            key: 105,
        }, {
            title: '106会议厅',
            key: 106,
        }, {
            title: '107会议厅',
            key: 107,
        }, {
            title: '108会议厅',
            key: 108,
        }, {
            title: '109会议厅',
            key: 109,
        }, {
            title: '201会议厅',
            key: 201,
        }, {
            title: '301会议厅',
            key: 301,
        }],
        dataSource = [{   ///行数据
            key: 101,
            tasks: [{
                key: 1,
                start: "07:00:00",
                end: "08:30:00",
                title: "面试"
            }, {
                key: 2,
                start: "09:00:00",
                end: "11:30:00",
                title: "面试"
            }]
        }, {
            key: 104,
            tasks: [{
                key: 1,
                start: "02:00:00",
                end: "11:30:00",
                title: "月度会"
            }, {
                key: 2,
                start: "12:20:00",
                end: "13:30:00",
                title: "周会"
            }]
        }, {
            key: 105,
            tasks: [{
                key: 1,
                start: "09:00:00",
                end: "10:30:00",
                title: "召开养易却参动少铁专火民的会议"
            }, {
                key: 2,
                start: "11:20:00",
                end: "13:30:00",
                title: "召开养易却参动少铁专火民的会议"
            }]
        }]
    } = props;
    const valueScrollRef = useRef();
    const timeScrollRef = useRef();
    const [offsetX, setOffsetX] = useState(0)
    const [offsetY, setOffsetY] = useState(0)
    const [translateX, setTranslateX] = useState(0)
    const [tooltipVisble, setTooltipVisble] = useState('none')
    const [tooltipData, setTooltipData] = useState(null)
    const onScrollY = e => {
        valueScrollRef.current.scrollTop = e.target.scrollTop
    }

    const onScrollX = e => {
        timeScrollRef.current.scrollLeft = e.target.scrollLeft
    }

    const onWheel = e => {
        timeScrollRef.current.scrollLeft += e.deltaY
        valueScrollRef.current.scrollLeft += e.deltaY
    }

    const onMouseEnter = (e, value) => {
        setTooltipData(value)
        setTooltipVisble('block')
    }

    const onMouseMove = e => {
        e.nativeEvent.stopImmediatePropagation();
        const offset = (e.clientX / document.body.clientWidth * 100).toFixed(0)
        setTranslateX(offset)
        setOffsetX(e.clientX)
        setOffsetY(e.clientY)
    }

    const onMouseLeave = e => {
        setTooltipVisble('none')
    }

    const timeInterval = (start, end) => {
        const t1 = new Date(`2017-1-1 ${start}`);
        const t2 = new Date(`2017-1-1 ${end}`);
        const interval = t2.getTime() - t1.getTime();
        if (interval < 0) return 0;
        return (interval / 1000 / 60 / 60).toFixed(2)
    }

    useEffect(() => {
        valueScrollRef.current.scrollLeft = hourWidth * startTime
    }, [])

    return (
        <>
            <div className={styles.container_wrapper} style={{ width }} >
                <div className={styles.container} style={{ display: 'block' }}>
                    <div className={styles.rowTitle} style={{ width: 150 }}>{rowTitle || null}</div>
                    <div className={styles.header_container} ref={timeScrollRef} style={{ marginLeft: 150 }}>
                        <div className={styles.time_header_container} >
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                00:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                01:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                02:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                03:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                04:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                05:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                06:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                07:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                08:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                09:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                10:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                11:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                12:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                13:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                14:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                15:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                16:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                17:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                18:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                19:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                20:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                21:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                22:00
                            </div>
                            <div className={styles.time_header_item} style={{ width: hourWidth }}>
                                23:00
                            </div>
                        </div>
                    </div>
                    <div className={styles.desc_container} onScroll={onScrollY} style={{ height: bodyHeight, width: 150, display: 'block' }}>
                        {rows.map(row => (
                            <div key={row.key} className={styles.row_desc_container} style={{ height: rowHeight, lineHeight: `${rowHeight}px` }}>
                                {row.title}
                            </div>
                        ))}
                    </div>
                    <div className={styles.val_container} ref={valueScrollRef} onScroll={onScrollX} onWheel={onWheel} style={{ height: bodyHeight, display: 'block' }}>
                        {rows.map(row => (
                            <div key={row.key} className={styles.row_val_container} style={{ height: rowHeight, lineHeight: `${rowHeight}px`, width: 24 * hourWidth }}>
                                {
                                    dataSource.find(data => data.key === row.key) ? dataSource.find(data => data.key === row.key).tasks.map(
                                        task => (
                                            <div
                                                key={task.key}
                                                onMouseEnter={e => onMouseEnter(e, task)}
                                                onMouseLeave={onMouseLeave}
                                                onMouseMove={onMouseMove}
                                                className={styles.activity}
                                                style={{
                                                    width: timeInterval(task.start, task.end) * hourWidth,
                                                    left: timeInterval('00:00:00', task.start) * hourWidth,
                                                    backgroundColor: '#5e63b5',
                                                    height: 24,
                                                }}
                                            />
                                        )
                                    ) : <></>
                                }
                            </div>
                        ))}
                    </div>
                </div>
            </div>
            <div className={styles.tooltip}
                style={{ left: offsetX, top: offsetY - 60, position: 'fixed', display: tooltipVisble, transform: `translateX(-${translateX}%)` }}
            >
                <div className={styles.title}>
                    {tooltipData ? tooltipData.title : ''}
                </div>
                <div className={styles.time}>
                    {`${tooltipData ? tooltipData.start : ''} - ${tooltipData ? tooltipData.end : ''}`}
                </div>
            </div>
        </>
    )
}

export default GunttChart;
.container_wrapper {
  position: relative;
  white-space: nowrap;
  font-family: basefontRegular, Helvetica Neue, Arial, sans-serif;
  font-size: 15px;
  color: #585050;
  height: 100%;

  .container {
    background-color: #b5bbbb;
    border-radius: 2px;
    height: 100%;
  }

  .rowTitle {
    display: block;
    float: left;
    background-color: #f0f0f0;
    border-right: solid 1px #e0e0e0;
    border-bottom: solid 1px #e0e0e0;
    font-weight: 600;
    height: 40px;
    line-height: 40px;
    text-align: center;
  }

  .header_container {
    overflow-x: hidden;
    overflow-y: hidden;
    position: relative;

    .time_header_container {
      width: 100%;
      white-space: nowrap;
      font-weight: bold;

      .time_header_item {
        height: 40px;
        line-height: 40px;
        display: inline-block;
        box-sizing: border-box;
        background-color: #f0f0f0;
        border-right: solid 1px #e0e0e0;
        border-bottom: solid 1px #e0e0e0;
        text-align: center;
        vertical-align: middle;
      }
    }
  }

  .desc_container {
    display: block;
    float: left;
    border-right: solid 2px #ccc;
    font-weight: bold;
    overflow-y: auto;

    .row_desc_container {
      display: block;
      padding-left: 10px;
    }

    .row_desc_container:nth-child(even) {
      background-color: #e0e0e0;
    }

    .row_desc_container:nth-child(odd) {
      background-color: #f0f0f0;
    }

    .row_desc_container:first-child {
      border-top-left-radius: 2px;
    }

    .row_desc_container:last-child {
      border-bottom-left-radius: 2px;
    }
  }

  .val_container {
    vertical-align: top;
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 0px;

    .row_val_container {
      position: relative;

      .activity {
        box-sizing: border-box;
        position: absolute;
        border-radius: 2px;
        padding: 0px 5px;
        top: 50%;
        transform: translateY(-50%);
        min-width: 1px;
        font-size: 14px;
        text-align: center;
        line-height: 24px;
        color: #fff;
        background-color: #5e63b5;
        cursor: pointer;
      }
    }

    .row_val_container:nth-child(even) {
      background-color: #e0e0e0;
    }

    .row_val_container:nth-child(odd) {
      background-color: #f0f0f0;
    }
  }
}

.tooltip {
  z-index: 99999;

  border-radius: 2px;
  background-color: #fff;
  box-shadow: 1px 1px 3px 3px rgba(0, 0, 0, 0.3);
  overflow: hidden;
  white-space: pre;

  .title {
    background-color: #718fbd;
    color: #fff;
    padding: 2px 6px;
    text-align: center;
    font-weight: 500;
  }

  .time {
    font-size: 14px;
    padding: 2px 6px;
    text-align: center;
    border: 1px solid #718fbd;
    border-radius: 0px 0px 2px 2px;
  }
}

.tooltip::before {
  position: absolute;
  left: -12px;
  width: 0;
  height: 0;
  border: 6px solid transparent;
  border-right: 6px solid#fff;
  content: '';
}

在线示例:https://codesandbox.io/s/nervous-pond-txexs3?file=/src/App.js

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容

  • 工作中遇到要使用甘特图的情况 简单记录下来 代码如下: html: js方法:initCharts2 () { ...
    阎燕茹阅读 3,868评论 0 0
  • 一、甘特图的作用 在做甘特图之前,需要明确自己希望甘特图能起到什么作用。如何你和我一样是因为领导需要你提供一份项目...
    唐醋鱼阅读 10,481评论 0 10
  • 项目中要用到甘特图,最开始研究了echart,实现的样式如下: echart实现甘特图.png 对比下面的原型图,...
    卷卷_毛阅读 492评论 0 0
  • 一、甘特图功能操作视频 二、甘特图界面 甘特图界面默认展示甘特图模板,通过编辑功能来修改甘特图内容,参照图2.1。...
    WindMale阅读 9,730评论 0 0
  • 甘特图适用的场景可以是在多个项目同时进行的情况下,在某个时间点可以直观的看见有哪些任务已经完成,哪些任务还待完成,...
    三三茶阅读 1,472评论 0 0