├── README.md ├── lerna.json ├── www ├── public │ ├── favicon.ico │ ├── index.html │ └── bundle.html ├── tsconfig.json ├── src │ ├── react-app-env.d.ts │ ├── index.tsx │ └── Example.tsx ├── package.json └── .kktrc.ts ├── .husky └── pre-commit ├── .prettierignore ├── .gitpod.yml ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── core ├── tsconfig.json ├── src │ ├── style │ │ └── index.less │ ├── index.tsx │ ├── Rect.tsx │ ├── LabelsWeek.tsx │ ├── Legend.tsx │ ├── utils.ts │ ├── LabelsMonth.tsx │ ├── Day.tsx │ └── SVG.tsx ├── .kktrc.ts ├── package.json └── README.md ├── .prettierrc ├── renovate.json ├── .gitignore ├── tsconfig.json ├── LICENSE └── package.json /README.md: -------------------------------------------------------------------------------- 1 | core/README.md -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.3.3", 3 | "packages": [ 4 | "core", 5 | "www" 6 | ] 7 | } -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiwjs/react-heat-map/HEAD/www/public/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.yml 5 | package.json 6 | node_modules 7 | dist 8 | build 9 | lib 10 | test 11 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ports: 2 | - port: 3000 3 | onOpen: open-preview 4 | tasks: 5 | - init: npm install 6 | command: npm run build && npm run start -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: jaywcjlove 2 | buy_me_a_coffee: jaywcjlove 3 | custom: ["https://www.paypal.me/kennyiseeyou", "https://jaywcjlove.github.io/#/sponsor"] 4 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "lib", 7 | "noEmit": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["./src", ".kktrc.ts"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "emitDeclarationOnly": true, 7 | "noEmit": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": ["*"], 7 | "rangeStrategy": "replace" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cjs 2 | esm 3 | lib 4 | build 5 | dist 6 | node_modules 7 | npm-debug.log* 8 | package-lock.json 9 | dist.css 10 | 11 | .eslintcache 12 | .DS_Store 13 | .cache 14 | .vscode 15 | 16 | *.bak 17 | *.tem 18 | *.temp 19 | #.swp 20 | *.*~ 21 | ~*.* 22 | 23 | .idea 24 | yarn.lock 25 | -------------------------------------------------------------------------------- /core/src/style/index.less: -------------------------------------------------------------------------------- 1 | @w-heatmap:~ "w-heatmap"; 2 | 3 | svg.@{w-heatmap} { 4 | rect { 5 | &:hover { 6 | stroke: var(--rhm-rect-hover-stroke, rgba(0, 0, 0, 0.14)); 7 | stroke-width: 1px; 8 | } 9 | &:active { 10 | fill: #196127; 11 | fill: var(--rhm-rect-active, #196127); 12 | stroke-width: 0; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /www/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare var VERSION: string; 4 | 5 | declare module '*.module.less' { 6 | const classes: { readonly [key: string]: string }; 7 | export default classes; 8 | } 9 | 10 | declare module '*.md' { 11 | import { CodeBlockData } from 'markdown-react-code-preview-loader'; 12 | const src: CodeBlockData; 13 | export default src; 14 | } 15 | -------------------------------------------------------------------------------- /core/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SVG, { SVGProps } from './SVG'; 3 | import './style/index.less'; 4 | 5 | export * from './SVG'; 6 | 7 | export interface HeatMapProps extends SVGProps { 8 | prefixCls?: string; 9 | } 10 | 11 | export default function HeatMap(props: HeatMapProps) { 12 | const { prefixCls = 'w-heatmap', className, ...others } = props; 13 | const cls = [className, prefixCls].filter(Boolean).join(' '); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "declaration": true, 16 | "baseUrl": ".", 17 | "jsx": "react-jsx", 18 | "noFallthroughCasesInSwitch": true, 19 | "noEmit": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/.kktrc.ts: -------------------------------------------------------------------------------- 1 | // import path from 'path'; 2 | import { Configuration } from 'webpack'; 3 | import { LoaderConfOptions } from 'kkt'; 4 | import lessModules from '@kkt/less-modules'; 5 | // import rawModules from '@kkt/raw-modules'; 6 | // import pkg from './package.json'; 7 | 8 | export default (conf: Configuration, env: 'production' | 'development', options: LoaderConfOptions) => { 9 | conf = lessModules(conf, env, options); 10 | if (options.bundle) { 11 | conf.output!.library = '@uiw/react-heat-map'; 12 | conf.externals = { 13 | react: { 14 | root: 'React', 15 | commonjs2: 'react', 16 | commonjs: 'react', 17 | amd: 'react', 18 | }, 19 | }; 20 | } 21 | return conf; 22 | }; 23 | -------------------------------------------------------------------------------- /www/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import MarkdownPreview from '@uiw/react-markdown-preview-example'; 3 | import data from '@uiw/react-heat-map/README.md'; 4 | import Demo from './Example'; 5 | 6 | const Github = MarkdownPreview.Github; 7 | const Example = MarkdownPreview.Example; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container!); // createRoot(container!) if you use TypeScript 11 | root.render( 12 | 20 | 21 | 22 | 23 | 24 | , 25 | ); -------------------------------------------------------------------------------- /core/src/Rect.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | import type { HeatMapValue, SVGProps } from './SVG'; 3 | import React from 'react'; 4 | 5 | export const rectStyle: CSSProperties = { 6 | display: 'block', 7 | cursor: 'pointer', 8 | } 9 | 10 | export interface RectProps extends React.SVGProps { 11 | value?: HeatMapValue & { 12 | column: number; 13 | row: number; 14 | index: number; 15 | }; 16 | render?: SVGProps['rectRender'] 17 | } 18 | 19 | export const Rect = (props: RectProps) => { 20 | const { style, value, render, key, ...reset} = props; 21 | const rectProps: React.SVGProps = { 22 | ...reset, 23 | style: { 24 | display: 'block', 25 | cursor: 'pointer', 26 | ...style, 27 | } 28 | } 29 | 30 | if (render && typeof render === 'function') { 31 | const elm = render({ ...rectProps }, value as Required['value']); 32 | if (elm && React.isValidElement(elm)) { 33 | return elm; 34 | } 35 | } 36 | 37 | return ; 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 uiw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HeatMap for React. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /core/src/LabelsWeek.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | import React, { Fragment, useMemo } from 'react'; 3 | import { SVGProps } from './SVG'; 4 | 5 | export const textStyle: CSSProperties = { 6 | textAnchor: 'middle', 7 | fontSize: 'inherit', 8 | fill: 'currentColor', 9 | } 10 | 11 | export interface LablesWeekProps extends React.SVGProps { 12 | weekLabels: SVGProps['weekLabels']; 13 | rectSize: SVGProps['rectSize']; 14 | space: SVGProps['space']; 15 | topPad: number; 16 | } 17 | export const LabelsWeek = ({ weekLabels = [], rectSize = 0, topPad = 0, space = 0 }: LablesWeekProps) => 18 | useMemo( 19 | () => ( 20 | 21 | {[...Array(7)].map((_, idx) => { 22 | if (weekLabels && weekLabels[idx]) { 23 | return ( 24 | 25 | {weekLabels[idx]} 26 | 27 | ); 28 | } 29 | return null; 30 | })} 31 | 32 | ), 33 | [rectSize, space, topPad, weekLabels], 34 | ); 35 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "private": true, 4 | "version": "2.3.3", 5 | "scripts": { 6 | "build": "kkt build", 7 | "start": "kkt start", 8 | "map": "source-map-explorer build/static/js/*.js --html build/website-result.html" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@kkt/less-modules": "^7.4.9", 13 | "@kkt/raw-modules": "^7.4.9", 14 | "@kkt/scope-plugin-options": "^7.4.9", 15 | "@types/katex": "^0.16.0", 16 | "@types/react": "^18.0.33", 17 | "@types/react-dom": "^18.0.11", 18 | "kkt": "^7.4.9", 19 | "markdown-react-code-preview-loader": "^2.1.2", 20 | "source-map-explorer": "^2.5.3" 21 | }, 22 | "dependencies": { 23 | "@uiw/react-heat-map": "2.3.3", 24 | "@uiw/react-markdown-preview-example": "^2.0.0", 25 | "@uiw/react-tooltip": "^4.21.11", 26 | "@uiw/reset.css": "^1.0.6", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/Legend.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useMemo } from 'react'; 2 | import { Rect, RectProps } from './Rect'; 3 | import { SVGProps } from './SVG'; 4 | 5 | export interface LegendProps extends RectProps { 6 | panelColors: SVGProps['panelColors']; 7 | rectSize: SVGProps['rectSize']; 8 | leftPad: number; 9 | rectY: number; 10 | legendCellSize: number; 11 | legendRender?: (props: RectProps) => React.ReactElement; 12 | topPad: number; 13 | space: number; 14 | } 15 | export default function Legend({ 16 | panelColors, 17 | leftPad = 0, 18 | topPad = 0, 19 | rectY = 15, 20 | space = 0, 21 | rectSize = 0, 22 | legendCellSize = 0, 23 | legendRender, 24 | ...props 25 | }: LegendProps) { 26 | let size = legendCellSize || rectSize; 27 | return useMemo( 28 | () => ( 29 | 30 | {Object.keys(panelColors || {}).map((num, key) => { 31 | const rectProps = { 32 | ...props, 33 | key, 34 | x: (size + 1) * key + leftPad, 35 | y: rectY, 36 | // y: topPad + rectSize * 8 + 6, 37 | fill: panelColors![Number(num)], 38 | width: size, 39 | height: size, 40 | }; 41 | if (legendRender) return legendRender(rectProps); 42 | return ; 43 | })} 44 | 45 | ), 46 | [panelColors, props, size, rectY, leftPad, rectSize, legendRender], 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "⬇️⬇️⬇️⬇️⬇️ package ⬇️⬇️⬇️⬇️⬇️": "▼▼▼▼▼ package ▼▼▼▼▼", 5 | "watch": "npm run-script watch --workspace @uiw/react-heat-map", 6 | "build": "npm run-script build --workspace @uiw/react-heat-map", 7 | "bundle": "npm run-script bundle --workspace @uiw/react-heat-map", 8 | "bundle:min": "npm run-script bundle:min --workspace @uiw/react-heat-map", 9 | "doc": "npm run-script build --workspace www", 10 | "start": "npm run-script start --workspace www", 11 | "⬆️⬆️⬆️⬆️⬆️ package ⬆️⬆️⬆️⬆️⬆️": "▲▲▲▲▲ package ▲▲▲▲▲", 12 | "version": "lerna version --exact --force-publish --no-push --no-git-tag-version", 13 | "prepare": "npm run build", 14 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 15 | "type-check": "tsc --noEmit", 16 | "map": "source-map-explorer build/static/js/*.js --html build/website-result.html" 17 | }, 18 | "author": "kenny wong ", 19 | "license": "MIT", 20 | "lint-staged": { 21 | "*.{js,jsx,tsx,ts,less,md,json}": [ 22 | "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"" 23 | ] 24 | }, 25 | "devDependencies": { 26 | "@kkt/less-modules": "^7.4.9", 27 | "@kkt/ncc": "^1.0.14", 28 | "compile-less-cli": "^1.8.13", 29 | "husky": "^8.0.3", 30 | "kkt": "^7.4.9", 31 | "lerna": "^8.0.0", 32 | "lint-staged": "^15.0.0", 33 | "prettier": "^3.0.0", 34 | "react-test-renderer": "^18.2.0", 35 | "tsbb": "^4.5.1" 36 | }, 37 | "workspaces": [ 38 | "core", 39 | "www" 40 | ], 41 | "engines": { 42 | "node": ">=16.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps, HeatMapValue } from './SVG'; 2 | 3 | export const oneDayTime = 24 * 60 * 60 * 1000; 4 | 5 | export function isValidDate(date: Date) { 6 | return date instanceof Date && !isNaN(date.getTime()); 7 | } 8 | 9 | export function getDateToString(date: Date) { 10 | return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; 11 | } 12 | 13 | export function formatData(data: SVGProps['value'] = []) { 14 | const result: Record = {}; 15 | data.forEach((item) => { 16 | if (item.date && isValidDate(new Date(item.date))) { 17 | item.date = getDateToString(new Date(item.date)); 18 | result[item.date] = item; 19 | } 20 | }); 21 | return result; 22 | } 23 | 24 | /** 排序 比较函数 */ 25 | export function numberSort(keys: number[] = []) { 26 | return keys.sort((x, y) => { 27 | if (x < y) return -1; 28 | else if (x > y) return 1; 29 | return 0; 30 | }); 31 | } 32 | 33 | export function existColor(num: number = 0, nums: number[], panelColors: Record = {}) { 34 | let color = ''; 35 | for (let a = 0; a < nums.length; a += 1) { 36 | if (nums[a] > num) { 37 | color = panelColors[nums[a]]; 38 | break; 39 | } 40 | color = panelColors[nums[a]]; 41 | } 42 | return color; 43 | } 44 | 45 | export const convertPanelColors = (colors: string[], maxCount: number): Record => { 46 | const step = Math.ceil(maxCount / (colors.length - 1)); 47 | const panelColors: Record = {}; 48 | colors.forEach((color, index) => { 49 | panelColors[index * step] = color; 50 | }); 51 | return panelColors; 52 | }; -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uiw/react-heat-map", 3 | "version": "2.3.3", 4 | "description": "React component create calendar heatmap to visualize time series data, a la github contribution graph.", 5 | "homepage": "https://uiwjs.github.io/react-heat-map/", 6 | "funding": "https://jaywcjlove.github.io/#/sponsor", 7 | "main": "./lib/index.js", 8 | "module": "./esm/index.js", 9 | "types": "./lib/index.d.ts", 10 | "author": "kenny wang ", 11 | "license": "MIT", 12 | "scripts": { 13 | "css:build": "compile-less -d src -o esm", 14 | "css:watch": "compile-less -d src -o esm --watch", 15 | "css:build:dist": "compile-less -d src --combine dist.css --rm-global", 16 | "bundle": "ncc build src/index.tsx --target web --filename heat-map", 17 | "bundle:watch": "ncc watch src/index.tsx --target web --filename heat-map", 18 | "bundle:min": "ncc build src/index.tsx --target web --filename heat-map --minify", 19 | "watch": "tsbb watch src/*.tsx --use-babel & npm run css:watch", 20 | "build": "tsbb build src/*.tsx --use-babel && npm run css:build && npm run css:build:dist", 21 | "type-check": "tsc --noEmit", 22 | "test": "tsbb test --env=jsdom", 23 | "coverage": "tsbb test --env=jsdom --coverage --bail" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/uiwjs/react-heat-map.git" 28 | }, 29 | "files": [ 30 | "dist.css", 31 | "dist", 32 | "src", 33 | "lib", 34 | "esm" 35 | ], 36 | "keywords": [ 37 | "react", 38 | "heat-map", 39 | "react-heat-map", 40 | "uiw", 41 | "uiwjs", 42 | "code" 43 | ], 44 | "peerDependencies": { 45 | "@babel/runtime": ">=7.10.0", 46 | "react": ">=16.9.0", 47 | "react-dom": ">=16.9.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/LabelsMonth.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useMemo } from 'react'; 2 | import { oneDayTime } from './utils'; 3 | import { SVGProps } from './SVG'; 4 | import { textStyle } from './LabelsWeek'; 5 | 6 | export interface LablesMonthProps extends React.SVGProps { 7 | monthLabels: SVGProps['monthLabels']; 8 | rectSize: SVGProps['rectSize']; 9 | space: SVGProps['space']; 10 | leftPad: number; 11 | colNum: number; 12 | rectY?: number; 13 | startDate: SVGProps['startDate']; 14 | endDate?: SVGProps['endDate']; 15 | } 16 | 17 | const generateData = (colNum: number, monthLabels: false | string[], startDate: Date, endDate?: Date) => { 18 | if (monthLabels === false || colNum < 1) return []; 19 | return Array.from({ length: colNum * 7 }) 20 | .map((_, idx) => { 21 | if ((idx / 7) % 1 === 0) { 22 | const date = new Date(startDate.getTime() + idx * oneDayTime); 23 | const month = date.getMonth(); 24 | if (endDate && date > endDate) return null; 25 | return { col: idx / 7, index: idx, month, day: date.getDate(), monthStr: monthLabels[month], date }; 26 | } 27 | return null; 28 | }) 29 | .filter(Boolean) 30 | .filter((item, idx, list) => list[idx - 1] && list[idx - 1]!.month !== item!.month); 31 | }; 32 | 33 | export const LabelsMonth = ({ 34 | monthLabels = [], 35 | rectSize = 0, 36 | space = 0, 37 | leftPad = 0, 38 | colNum = 0, 39 | rectY = 15, 40 | startDate, 41 | endDate, 42 | }: LablesMonthProps) => { 43 | 44 | const data = useMemo(() => generateData(colNum, monthLabels, startDate!, endDate), [colNum, monthLabels, startDate, endDate]); 45 | return ( 46 | 47 | {data.map((item, idx) => ( 48 | 57 | {item!.monthStr} 58 | 59 | ))} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /core/src/Day.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useMemo } from "react" 2 | import { Rect, RectProps } from './Rect'; 3 | import { formatData, getDateToString, existColor, numberSort, oneDayTime } from './utils'; 4 | import { SVGProps } from './SVG'; 5 | 6 | type DayProps = { 7 | transform?: string; 8 | gridNum?: number; 9 | initStartDate: Date; 10 | endDate?: Date; 11 | rectProps?: RectProps; 12 | rectSize?: number; 13 | space?: number; 14 | startY?: number; 15 | rectRender?: SVGProps['rectRender']; 16 | panelColors?: SVGProps['panelColors']; 17 | value?: SVGProps['value']; 18 | } 19 | 20 | export const Day:FC> = (props) => { 21 | const { transform, gridNum = 0, startY = 0, panelColors = {}, initStartDate, space = 2, value = [], rectSize = 11, endDate, rectProps, rectRender } = props; 22 | const data = useMemo(() => formatData(value), [value]); 23 | const nums = useMemo(() => numberSort(Object.keys(panelColors).map((item) => parseInt(item, 10))), [panelColors]); 24 | return ( 25 | 26 | {gridNum > 0 && 27 | [...Array(gridNum)].map((_, idx) => { 28 | return ( 29 | 30 | {[...Array(7)].map((_, cidx) => { 31 | const currentDate = new Date(initStartDate.getTime() + oneDayTime * (idx * 7 + cidx)); 32 | const date = getDateToString(currentDate); 33 | const dataProps: RectProps['value'] = { 34 | ...data[date], 35 | date: date, 36 | row: cidx, 37 | column: idx, 38 | index: idx * 7 + cidx, 39 | }; 40 | const dayProps: RectProps = { 41 | ...rectProps, 42 | fill: 'var(--rhm-rect, #EBEDF0)', 43 | width: rectSize, 44 | height: rectSize, 45 | x: idx * (rectSize + space), 46 | y: (rectSize + space) * cidx, 47 | render: rectRender, 48 | value: dataProps 49 | }; 50 | 51 | if (endDate instanceof Date && currentDate.getTime() > endDate.getTime()) { 52 | return null; 53 | } 54 | if (date && data[date] && panelColors && Object.keys(panelColors).length > 0) { 55 | dayProps.fill = existColor(data[date].count || 0, nums, panelColors); 56 | } else if (panelColors && panelColors[0]) { 57 | dayProps.fill = panelColors[0]; 58 | } 59 | return ( 60 | 69 | ); 70 | })} 71 | 72 | ); 73 | })} 74 | 75 | ) 76 | } -------------------------------------------------------------------------------- /www/.kktrc.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { LoaderConfOptions, WebpackConfiguration } from 'kkt'; 4 | import lessModules from '@kkt/less-modules'; 5 | import scopePluginOptions from '@kkt/scope-plugin-options'; 6 | import { mdCodeModulesLoader } from 'markdown-react-code-preview-loader'; 7 | import pkg from './package.json'; 8 | 9 | export default (conf: WebpackConfiguration, env: 'production' | 'development', options: LoaderConfOptions) => { 10 | conf = lessModules(conf, env, options); 11 | conf = mdCodeModulesLoader(conf); 12 | conf = scopePluginOptions(conf, env, { 13 | ...options, 14 | allowedFiles: [path.resolve(process.cwd(), 'README.md')], 15 | }); 16 | // Get the project version. 17 | conf.plugins!.push( 18 | new webpack.DefinePlugin({ 19 | VERSION: JSON.stringify(pkg.version), 20 | }), 21 | ); 22 | conf.module!.exprContextCritical = false; 23 | conf.ignoreWarnings = [ 24 | { module: /node_modules[\\/]parse5[\\/]/, } 25 | ]; 26 | 27 | if (env === 'production') { 28 | conf.optimization = { 29 | ...conf.optimization, 30 | splitChunks: { 31 | cacheGroups: { 32 | reactvendor: { 33 | test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, 34 | name: 'react-vendor', 35 | chunks: 'all', 36 | }, 37 | katex: { 38 | test: /[\\/]node_modules[\\/](katex)[\\/]/, 39 | name: 'katex-vendor', 40 | chunks: 'all', 41 | }, 42 | mermaid: { 43 | test: /[\\/]node_modules[\\/](mermaid)[\\/]/, 44 | name: 'mermaid-vendor', 45 | chunks: 'all', 46 | }, 47 | dagred3: { 48 | test: /[\\/]node_modules[\\/](dagre-d3)[\\/]/, 49 | name: 'dagre-d3-vendor', 50 | chunks: 'all', 51 | }, 52 | momentlodash: { 53 | test: /[\\/]node_modules[\\/](moment-mini|lodash|d3-array|d3-geo|d3-shape|dagre)[\\/]/, 54 | name: 'momentlodash-vendor', 55 | chunks: 'all', 56 | }, 57 | d3: { 58 | test: /[\\/]node_modules[\\/](d3-\w+(-?\w+))[\\/]/, 59 | name: 'd3-vendor', 60 | chunks: 'all', 61 | }, 62 | micromark: { 63 | test: /[\\/]node_modules[\\/](micromark)[\\/]/, 64 | name: 'micromark-vendor', 65 | chunks: 'all', 66 | }, 67 | prismjs: { 68 | test: /[\\/]node_modules[\\/](refractor)[\\/]/, 69 | name: 'refractor-prismjs-vendor', 70 | chunks: 'all', 71 | }, 72 | runtime: { 73 | test: /[\\/]node_modules[\\/](@babel[\\/]runtime)[\\/]/, 74 | name: 'babel-runtime-vendor', 75 | chunks: 'all', 76 | }, 77 | parse5: { 78 | test: /[\\/]node_modules[\\/](parse5)[\\/]/, 79 | name: 'parse5-vendor', 80 | chunks: 'all', 81 | }, 82 | }, 83 | }, 84 | }; 85 | conf.output = { ...conf.output, publicPath: './' }; 86 | } 87 | return conf; 88 | }; 89 | -------------------------------------------------------------------------------- /www/public/bundle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HeatMap for React. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @uiw/react-heat-map 16 |
17 | 18 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | # paths-ignore: 7 | # - '.github/**/*.yml' 8 | 9 | jobs: 10 | build-deploy: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - run: npm install 23 | - run: npm run build 24 | - run: npm run bundle 25 | - run: npm run bundle:min 26 | - run: npm run doc 27 | 28 | - working-directory: core 29 | run: | 30 | npm run type-check 31 | 32 | - name: Generate Contributors Images 33 | uses: jaywcjlove/github-action-contributors@main 34 | with: 35 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\]) 36 | output: www/build/CONTRIBUTORS.svg 37 | avatarSize: 42 38 | 39 | - name: Create Tag 40 | id: create_tag 41 | uses: jaywcjlove/create-tag-action@main 42 | with: 43 | package-path: ./core/package.json 44 | 45 | - name: get tag version 46 | id: tag_version 47 | uses: jaywcjlove/changelog-generator@main 48 | 49 | - name: Deploy 50 | uses: peaceiris/actions-gh-pages@v4 51 | if: github.ref == 'refs/heads/main' 52 | with: 53 | commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }} 54 | github_token: ${{ secrets.GITHUB_TOKEN }} 55 | publish_dir: ./www/build 56 | user_name: 'github-actions[bot]' 57 | user_email: 'github-actions[bot]@users.noreply.github.com' 58 | 59 | - name: Generate Changelog 60 | id: changelog 61 | uses: jaywcjlove/changelog-generator@main 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | head-ref: ${{steps.create_tag.outputs.version}} 65 | filter-author: (renovate-bot|Renovate Bot) 66 | filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}' 67 | 68 | - name: Create Release 69 | uses: ncipollo/release-action@v1 70 | if: steps.create_tag.outputs.successful 71 | with: 72 | allowUpdates: true 73 | token: ${{ secrets.GITHUB_TOKEN }} 74 | name: ${{ steps.create_tag.outputs.version }} 75 | tag: ${{ steps.create_tag.outputs.version }} 76 | body: | 77 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) [![](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/@uiw/react-heat-map@${{steps.create_tag.outputs.versionNumber}}/file/README.md) [![](https://img.shields.io/github/issues/uiwjs/react-heat-map.svg)](https://github.com/uiwjs/react-heat-map/releases) [![](https://img.shields.io/github/forks/uiwjs/react-heat-map.svg)](https://github.com/uiwjs/react-heat-map/network) [![](https://img.shields.io/github/stars/uiwjs/react-heat-map.svg)](https://github.com/uiwjs/react-heat-map/stargazers) [![](https://img.shields.io/github/release/uiwjs/react-heat-map.svg)](https://github.com/uiwjs/react-heat-map/releases) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@uiw/react-heat-map)](https://bundlephobia.com/result?p=@uiw/react-heat-map@${{steps.create_tag.outputs.versionNumber}}) 78 | 79 | ```bash 80 | npm i @uiw/react-heat-map@${{steps.create_tag.outputs.versionNumber}} 81 | ``` 82 | 83 | ${{ steps.changelog.outputs.compareurl }} 84 | 85 | ${{ steps.changelog.outputs.changelog }} 86 | 87 | 88 | - name: 📦 @uiw/react-heat-map publish to NPM 89 | run: npm publish --access public --provenance 90 | continue-on-error: true 91 | working-directory: core 92 | env: 93 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /core/src/SVG.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; 2 | import { LabelsWeek } from './LabelsWeek'; 3 | import { LabelsMonth } from './LabelsMonth'; 4 | import { RectProps } from './Rect'; 5 | import { isValidDate, oneDayTime, convertPanelColors } from './utils'; 6 | import Legend, { LegendProps } from './Legend'; 7 | import { Day } from './Day'; 8 | 9 | export type HeatMapValue = { 10 | date: string; 11 | content?: string | string[] | React.ReactNode; 12 | count: number; 13 | }; 14 | 15 | export interface SVGProps extends React.SVGProps { 16 | startDate?: Date; 17 | endDate?: Date; 18 | rectSize?: number; 19 | legendCellSize?: number; 20 | space?: number; 21 | rectProps?: RectProps; 22 | legendRender?: LegendProps['legendRender']; 23 | rectRender?: ( 24 | data: React.SVGProps, 25 | valueItem: HeatMapValue & { 26 | column: number; 27 | row: number; 28 | index: number; 29 | }, 30 | ) => React.ReactElement | void; 31 | value?: Array; 32 | weekLabels?: string[] | false; 33 | monthLabels?: string[] | false; 34 | /** position of month labels @default `top` */ 35 | monthPlacement?: 'top' | 'bottom'; 36 | panelColors?: Record | string[]; 37 | } 38 | 39 | export default function SVG(props: SVGProps) { 40 | const { 41 | rectSize = 11, 42 | legendCellSize = 11, 43 | space = 2, 44 | monthPlacement = 'top', 45 | startDate = new Date(), 46 | endDate, 47 | rectProps, 48 | rectRender, 49 | legendRender, 50 | value = [], 51 | weekLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 52 | monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 53 | panelColors = ['var(--rhm-rect, #EBEDF0)','#C6E48B','#7BC96F', '#239A3B', '#196127'], 54 | style, 55 | ...other 56 | } = props || {}; 57 | 58 | const maxCount = Math.max(...value.map(item => item.count), 0); 59 | const panelColorsObject = Array.isArray(panelColors) ? convertPanelColors(panelColors, maxCount) : panelColors; 60 | const [gridNum, setGridNum] = useState(0); 61 | const [leftPad, setLeftPad] = useState(!!weekLabels ? 28 : 5); 62 | 63 | const defaultTopPad = monthPlacement === 'top' ? 20 : 5; 64 | const [topPad, setTopPad] = useState(!!monthLabels ? defaultTopPad : 5); 65 | const svgRef = React.createRef(); 66 | useEffect(() => setLeftPad(!!weekLabels ? 28 : 5), [weekLabels]); 67 | useEffect(() => { 68 | if (svgRef.current) { 69 | const width = svgRef.current.clientWidth - leftPad || 0; 70 | setGridNum(Math.floor(width / (rectSize + space)) || 0); 71 | } 72 | }, [rectSize, svgRef, space, leftPad]); 73 | 74 | useEffect(() => { 75 | setTopPad(!!monthLabels ? defaultTopPad : 5); 76 | }, [monthLabels]); 77 | 78 | const initStartDate = useMemo(() => { 79 | if (isValidDate(startDate)) { 80 | return !startDate.getDay() ? startDate : new Date(startDate.getTime() - startDate.getDay() * oneDayTime); 81 | } else { 82 | const newDate = new Date(); 83 | return new Date(newDate.getTime() - newDate.getDay() * oneDayTime); 84 | } 85 | }, [startDate]); 86 | 87 | const styl = { 88 | color: 'var(--rhm-text-color, #24292e)', 89 | userSelect: 'none', 90 | display: 'block', 91 | fontSize: 10, 92 | } as CSSProperties; 93 | 94 | const monthRectY = monthPlacement === 'top' ? 15 : 15 * 7 + space 95 | const legendTopPad = monthPlacement === 'top' ? topPad + rectSize * 8 + 6 : (!!monthLabels ? (topPad + rectSize + space) : topPad) + rectSize * 8 + 6; 96 | return ( 97 | 98 | {legendCellSize !== 0 && ( 99 | 109 | )} 110 | 111 | 121 | 133 | 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /www/src/Example.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Tooltip from '@uiw/react-tooltip'; 3 | import HeatMap, { HeatMapValue } from '@uiw/react-heat-map'; 4 | import styled from 'styled-components'; 5 | 6 | const ExampleWrapper = styled.div` 7 | background-color: #fff; 8 | border-radius: 5px; 9 | box-shadow: 0 0 0 1px rgb(16 22 26 / 10%), 0 0 0 rgb(16 22 26 / 0%), 0 1px 1px rgb(16 22 26 / 20%); 10 | margin: 0 auto; 11 | margin-top: 70px; 12 | width: 663px; 13 | svg { 14 | border-radius: 5px; 15 | } 16 | `; 17 | 18 | const Tools = styled.div` 19 | user-select: none; 20 | font-size: 12px; 21 | margin-top: 10px !important; 22 | padding: 10px; 23 | padding-left: 0px; 24 | border-radius: 5px; 25 | width: 663px; 26 | margin: 0 auto; 27 | label { 28 | display: flex; 29 | align-items: center; 30 | input { 31 | margin-right: 5px; 32 | margin-left: 10px; 33 | } 34 | } 35 | `; 36 | 37 | const Wrapper = styled.div` 38 | display: flex; 39 | flex-direction: column; 40 | `; 41 | 42 | const data1: HeatMapValue[] = [ 43 | { date: '2016/01/11', count: 2, content: '' }, 44 | ...[...Array(17)].map((_, idx) => ({ date: `2016/02/${idx + 10}`, count: idx, content: '' })), 45 | { date: '2016/03/02', count: 5, content: '' }, 46 | { date: '2016/03/04', count: 11, content: '' }, 47 | { date: '2016/03/14', count: 31, content: '' }, 48 | { date: '2016/03/16', count: 2, content: '' }, 49 | { date: '2016/04/11', count: 2, content: '' }, 50 | { date: '2016/05/01', count: 5, content: '' }, 51 | { date: '2016/05/02', count: 5, content: '' }, 52 | { date: '2016/05/04', count: 11, content: '' }, 53 | { date: '2016/05/14', count: 31, content: '' }, 54 | { date: '2016/05/16', count: 2, content: '' }, 55 | { date: '2016/05/17', count: 2, content: '' }, 56 | { date: '2016/05/18', count: 2, content: '' }, 57 | { date: '2016/05/19', count: 8, content: '' }, 58 | { date: '2016/05/20', count: 6, content: '' }, 59 | { date: '2016/05/21', count: 41, content: '' }, 60 | { date: '2016/05/22', count: 6, content: '' }, 61 | { date: '2016/06/11', count: 2, content: '' }, 62 | { date: '2016/07/01', count: 5, content: '' }, 63 | { date: '2016/07/02', count: 5, content: '' }, 64 | { date: '2016/07/04', count: 11, content: '' }, 65 | { date: '2016/07/14', count: 31, content: '' }, 66 | { date: '2016/07/16', count: 2, content: '' }, 67 | { date: '2016/07/17', count: 2, content: '' }, 68 | { date: '2016/07/18', count: 2, content: '' }, 69 | { date: '2016/07/19', count: 8, content: '' }, 70 | { date: '2016/07/20', count: 6, content: '' }, 71 | { date: '2016/07/21', count: 41, content: '' }, 72 | { date: '2016/07/22', count: 6, content: '' }, 73 | ...[...Array(17)].map((_, idx) => ({ date: `2016/08/${idx + 10}`, count: idx, content: '' })), 74 | ]; 75 | const data2: HeatMapValue[] = [ 76 | { date: '2016/04/02', count: 5, content: '' }, 77 | { date: '2016/04/04', count: 11, content: '' }, 78 | { date: '2016/04/14', count: 31, content: '' }, 79 | { date: '2016/04/16', count: 2, content: '' }, 80 | { date: '2016/04/17', count: 2, content: '' }, 81 | { date: '2016/04/18', count: 2, content: '' }, 82 | { date: '2016/04/19', count: 8, content: '' }, 83 | { date: '2016/04/11', count: 2, content: '' }, 84 | { date: '2016/04/01', count: 5, content: '' }, 85 | { date: '2016/04/02', count: 5, content: '' }, 86 | { date: '2016/04/04', count: 11, content: '' }, 87 | { date: '2016/04/14', count: 31, content: '' }, 88 | { date: '2016/04/16', count: 2, content: '' }, 89 | { date: '2016/04/17', count: 2, content: '' }, 90 | { date: '2016/04/18', count: 2, content: '' }, 91 | { date: '2016/04/19', count: 8, content: '' }, 92 | { date: '2016/04/20', count: 6, content: '' }, 93 | { date: '2016/04/21', count: 41, content: '' }, 94 | { date: '2016/04/22', count: 6, content: '' }, 95 | ]; 96 | 97 | const darkColor = { 0: 'rgb(255 255 255 / 25%)', 8: '#7BC96F', 4: '#C6E48B', 12: '#239A3B', 32: '#ff7b00' }; 98 | 99 | export default function Example() { 100 | const [value, setValue] = useState(data1); 101 | const [selectDate, setSelectDate] = useState(); 102 | const [enableEndDate, setEnableEndDate] = useState(false); 103 | const [enableDark, setEnableDark] = useState(false); 104 | const [enableCircle, setEnableCircle] = useState(false); 105 | const [rectSize, setRectSize] = useState(11); 106 | const [monthPlacement, setMonthPlacement] = useState<'top' | 'bottom'>('top'); 107 | const [legendCellSize, setLegendCellSize] = useState(); 108 | const [enableWeekLabels, setEnableWeekLabels] = useState(undefined); 109 | const [enableMonthLabels, setEnableMonthLabels] = useState(undefined); 110 | return ( 111 | 112 | 113 | { 131 | setSelectDate((e.target as any).dataset.date); 132 | }, 133 | }} 134 | legendRender={(props) => } 135 | rectRender={(props, data) => { 136 | // if (!data.count) return ; 137 | return ( 138 | 139 | 140 | 141 | ); 142 | }} 143 | // rectRender={({ 144 | // rectSize, 145 | // column, 146 | // space, 147 | // row, 148 | // fill, 149 | // date, 150 | // rx, 151 | // ...props 152 | // }: RectDayElement) => { 153 | // if (!enableCircle) return undefined; 154 | // return ( 155 | // 164 | // ); 165 | // }} 166 | /> 167 | 168 | 169 |
170 | 171 | 172 | {selectDate} 173 |
174 | 178 | 182 | 183 | 187 | 188 | 197 | 206 | 215 | 216 | 226 | 235 | 246 | 247 |
248 | 252 | 256 | 260 |
261 | 262 |
263 | 269 | 277 |
278 |
279 |
280 | ); 281 | } 282 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 |
2 | Using my app is also a way to support me: 3 |
4 | Deskmark 5 | Keyzer 6 | Vidwall Hub 7 | Deskmark 8 | Keyzer 9 | Vidwall Hub 10 | VidCrop 11 | Vidwall 12 | Mousio Hint 13 | Mousio 14 | Musicer 15 | Audioer 16 | FileSentinel 17 | FocusCursor 18 | Videoer 19 | KeyClicker 20 | DayBar 21 | Iconed 22 | Mousio 23 | Quick RSS 24 | Quick RSS 25 | Web Serve 26 | Copybook Generator 27 | DevTutor for SwiftUI 28 | RegexMate 29 | Time Passage 30 | Iconize Folder 31 | Textsound Saver 32 | Create Custom Symbols 33 | DevHub 34 | Resume Revise 35 | Palette Genius 36 | Symbol Scribe 37 |
38 |
39 | 40 | HeatMap 日历热图 41 | === 42 | 43 | 44 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 45 | [![Build & Deploy](https://github.com/uiwjs/react-heat-map/actions/workflows/ci.yml/badge.svg)](https://github.com/uiwjs/react-heat-map/actions/workflows/ci.yml) 46 | [![Coverage Status](https://img.shields.io/npm/dm/@uiw/react-heat-map.svg?style=flat)](https://www.npmjs.com/package/@uiw/react-heat-map) 47 | [![npm version](https://img.shields.io/npm/v/@uiw/react-heat-map.svg)](https://www.npmjs.com/package/@uiw/react-heat-map) 48 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@uiw/react-heat-map)](https://bundlephobia.com/result?p=@uiw/react-heat-map) 49 | [![Open in Gitpod](https://shields.io/badge/Open%20in-Gitpod-green?logo=Gitpod)](https://gitpod.io/#https://github.com/uiwjs/react-codemirror) 50 | 51 | 52 | A lightweight calendar heatmap react component built on SVG, customizable version of GitHub's contribution graph. Try it out on [website example](https://uiwjs.github.io/react-heat-map/). 53 | 54 | 55 | 56 | [![](https://user-images.githubusercontent.com/1680273/186116433-d58c2b6d-8468-4322-943c-9b63c2e447e4.png)](https://uiwjs.github.io/react-heat-map) 57 | 58 | 59 | 60 | ## Install 61 | 62 | ```bash 63 | # Not dependent on uiw. 64 | npm install @uiw/react-heat-map --save 65 | ``` 66 | If using Next.js, you will need to use the [`next-remove-imports`](https://www.npmjs.com/package/next-remove-imports) package to avoid errors, see [issue #69](https://github.com/uiwjs/react-heat-map/issues/69). 67 | 68 | ## Basic Usage 69 | 70 | Basic usage example, Please pay warning to the time setting. 71 | 72 | ⚠️ Example: ~~`2016-01-11`~~ -> `2016/01/11`, Support `Safari` 73 | 74 | ```jsx mdx:preview 75 | import React from 'react'; 76 | import HeatMap from '@uiw/react-heat-map'; 77 | 78 | const value = [ 79 | { date: '2016/01/11', count: 2 }, 80 | { date: '2016/01/12', count: 20 }, 81 | { date: '2016/01/13', count: 10 }, 82 | ...[...Array(17)].map((_, idx) => ({ 83 | date: `2016/02/${idx + 10}`, count: idx, content: '' 84 | })), 85 | { date: '2016/04/11', count: 2 }, 86 | { date: '2016/05/01', count: 5 }, 87 | { date: '2016/05/02', count: 5 }, 88 | { date: '2016/05/04', count: 11 }, 89 | ]; 90 | 91 | const Demo = () => { 92 | return ( 93 |
94 | 99 |
100 | ) 101 | }; 102 | 103 | export default Demo 104 | ``` 105 | 106 | ## Set Color 107 | 108 | Set the theme color style. 109 | 110 | ```jsx mdx:preview 111 | import React from 'react'; 112 | import HeatMap from '@uiw/react-heat-map'; 113 | 114 | const value = [ 115 | { date: '2016/01/11', count:2 }, 116 | { date: '2016/04/12', count:2 }, 117 | { date: '2016/05/01', count:17 }, 118 | { date: '2016/05/02', count:5 }, 119 | { date: '2016/05/03', count:27 }, 120 | { date: '2016/05/04', count:11 }, 121 | { date: '2016/05/08', count:32 }, 122 | ]; 123 | 124 | const Demo = () => { 125 | return ( 126 | 140 | ) 141 | }; 142 | export default Demo 143 | ``` 144 | 145 | Dynamic color based on maximum value 146 | 147 | ```jsx mdx:preview 148 | import React from 'react'; 149 | import HeatMap from '@uiw/react-heat-map'; 150 | 151 | const value = [ 152 | { date: '2016/01/11', count:2 }, 153 | { date: '2016/04/12', count:2 }, 154 | { date: '2016/05/01', count:17 }, 155 | { date: '2016/05/02', count:5 }, 156 | { date: '2016/05/03', count:27 }, 157 | { date: '2016/05/04', count:11 }, 158 | { date: '2016/05/08', count:32 }, 159 | ]; 160 | 161 | const Demo = () => { 162 | return ( 163 | 170 | ) 171 | }; 172 | export default Demo 173 | ``` 174 | 175 | ## Set Rect Style 176 | 177 | Set the radius of the rect. 178 | 179 | ```jsx mdx:preview 180 | import React, { useState } from 'react'; 181 | import HeatMap from '@uiw/react-heat-map'; 182 | 183 | const value = [ 184 | { date: '2016/01/11', count:2 }, 185 | ...[...Array(17)].map((_, idx) => ({ date: `2016/01/${idx + 10}`, count: idx })), 186 | ...[...Array(17)].map((_, idx) => ({ date: `2016/02/${idx + 10}`, count: idx })), 187 | { date: '2016/04/12', count:2 }, 188 | { date: '2016/05/01', count:5 }, 189 | { date: '2016/05/02', count:5 }, 190 | { date: '2016/05/03', count:1 }, 191 | { date: '2016/05/04', count:11 }, 192 | { date: '2016/05/08', count:32 }, 193 | ]; 194 | 195 | const Demo = () => { 196 | const [range, setRange] = useState(5) 197 | return ( 198 |
199 | setRange(e.target.value)} 206 | /> {range} 207 | } 213 | rectProps={{ 214 | rx: range 215 | }} 216 | /> 217 |
218 | ) 219 | }; 220 | export default Demo 221 | ``` 222 | 223 | ## Tooltip 224 | 225 | A simple text popup tip. 226 | 227 | ```jsx mdx:preview 228 | import React from 'react'; 229 | import Tooltip from '@uiw/react-tooltip'; 230 | import HeatMap from '@uiw/react-heat-map'; 231 | 232 | const value = [ 233 | { date: '2016/01/11', count:2 }, 234 | ...[...Array(17)].map((_, idx) => ({ date: `2016/01/${idx + 10}`, count: idx, })), 235 | ...[...Array(17)].map((_, idx) => ({ date: `2016/02/${idx + 10}`, count: idx, })), 236 | { date: '2016/04/12', count:2 }, 237 | { date: '2016/05/01', count:5 }, 238 | { date: '2016/05/02', count:5 }, 239 | { date: '2016/05/03', count:1 }, 240 | { date: '2016/05/04', count:11 }, 241 | { date: '2016/05/08', count:32 }, 242 | ]; 243 | 244 | const Demo = () => { 245 | return ( 246 | { 251 | // if (!data.count) return ; 252 | return ( 253 | 254 | 255 | 256 | ); 257 | }} 258 | /> 259 | ) 260 | }; 261 | export default Demo 262 | ``` 263 | 264 | ## Show/Hide Legend 265 | 266 | ```jsx mdx:preview 267 | import React, { useState } from 'react'; 268 | import HeatMap from '@uiw/react-heat-map'; 269 | 270 | const value = [ 271 | { date: '2016/01/11', count:2 }, 272 | ...[...Array(17)].map((_, idx) => ({ date: `2016/01/${idx + 10}`, count: idx })), 273 | ...[...Array(17)].map((_, idx) => ({ date: `2016/02/${idx + 10}`, count: idx })), 274 | { date: '2016/04/12', count:2 }, 275 | { date: '2016/05/01', count:5 }, 276 | { date: '2016/05/02', count:5 }, 277 | { date: '2016/05/03', count:1 }, 278 | { date: '2016/05/04', count:11 }, 279 | { date: '2016/05/08', count:32 }, 280 | ]; 281 | 282 | const Demo = () => { 283 | const [size, setSize] = useState(0) 284 | return ( 285 |
286 | 294 | 300 |
301 | ) 302 | }; 303 | export default Demo 304 | ``` 305 | 306 | ## Selected Rect 307 | 308 | ```jsx mdx:preview 309 | import React, { useState } from 'react'; 310 | import HeatMap from '@uiw/react-heat-map'; 311 | 312 | const value = [ 313 | { date: '2016/01/11', count:2 }, 314 | ...[...Array(17)].map((_, idx) => ({ date: `2016/01/${idx + 10}`, count: idx })), 315 | ...[...Array(17)].map((_, idx) => ({ date: `2016/02/${idx + 10}`, count: idx })), 316 | { date: '2016/04/12', count:2 }, 317 | { date: '2016/05/01', count:5 }, 318 | { date: '2016/05/02', count:5 }, 319 | { date: '2016/05/03', count:1 }, 320 | { date: '2016/05/04', count:11 }, 321 | { date: '2016/05/08', count:32 }, 322 | ]; 323 | 324 | const Demo = () => { 325 | const [selected, setSelected] = useState('') 326 | return ( 327 |
328 | { 333 | if (selected !== '') { 334 | props.opacity = data.date === selected ? 1 : 0.45 335 | } 336 | return ( 337 | { 338 | setSelected(data.date === selected ? '' : data.date); 339 | }} /> 340 | ); 341 | }} 342 | /> 343 |
344 | ) 345 | }; 346 | export default Demo 347 | ``` 348 | 349 | ## Props 350 | 351 | | Property | Description | Type | Default | 352 | | ---- | ---- | ---- | ---- | 353 | | `value` | Data to be displayed, **required** | Array | `[]` | 354 | | `rectSize` | Grid size | number | `11` | 355 | | `legendCellSize` | Size of the legend cells, in pixel. Value equal to `0` hide legend. | number | `11` | 356 | | `startDate` | Start date | Date | `new Date()` | 357 | | `endDate` | End date | Date | - | 358 | | `space` | Interval between grid sizes | number | `2` | 359 | | `monthPlacement` | position of month labels | `'top' | 'bottom'` | `top` | 360 | | `rectProps` | Grid node attribute settings | `React.SVGProps` | `2` | 361 | | `weekLabels` | Week display | string[] | `['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']` | 362 | | `monthLabels` | Month display | string[] | `['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']` | 363 | | `panelColors` | Backgroud color of active colors | `Record` \| `string[]` | `['var(--rhm-rect, #EBEDF0)','#C6E48B','#7BC96F', '#239A3B', '#196127']` | 364 | | `rectRender` | Single `day` block re-render | `(data: E & { key: number }, valueItem: HeatMapValue & { date: string, column: number, row: number, index: number }) => React.ReactElement` | - | 365 | | `legendRender` | Single `legend` block re-render | `(props: React.SVGProps) => React.ReactNode` | - | 366 | 367 | ## Development 368 | 369 | **`development`** 370 | 371 | Runs the project in development mode. 372 | 373 | ```bash 374 | npm install 375 | ``` 376 | 377 | ```bash 378 | # Step 1, run first, listen to the component compile and output the .js file 379 | # listen for compilation output type .d.ts file 380 | npm run watch 381 | # Step 2, development mode, listen to compile preview website instance 382 | npm run start 383 | ``` 384 | 385 | **`production`** 386 | 387 | Builds the app for production to the build folder. 388 | 389 | ```bash 390 | npm run build 391 | npm run doc 392 | ``` 393 | 394 | The build is minified and the filenames include the hashes. 395 | Your app is ready to be deployed! 396 | 397 | ## Contributors 398 | 399 | As always, thanks to our amazing contributors! 400 | 401 | 402 | 403 | 404 | 405 | Made with [github-action-contributors](https://github.com/jaywcjlove/github-action-contributors). 406 | 407 | ## License 408 | 409 | Licensed under the MIT License. 410 | 411 | --------------------------------------------------------------------------------