11 |
12 |
13 | 0
14 |
15 |
16 |
17 |
18 | {"1 - " + Math.round(highestTotal / 4 * 1)}
19 |
20 |
21 |
22 |
23 | {Math.round(highestTotal / 4 * 1) + " - " + Math.round(highestTotal / 4 * 2)}
24 |
25 |
26 |
27 |
28 | {Math.round(highestTotal / 4 * 2) + " - " + Math.round(highestTotal / 4 * 3)}
29 |
30 |
31 |
32 |
33 | {Math.round(highestTotal / 4 * 3) + " - " + highestTotal}
34 |
35 |
36 |
;
37 | }
38 |
39 | export default {gradient, box};
40 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import findMonthString from "./monthToString";
3 | import heatMapScale from "./heatMapScale";
4 | import dateHandle from "./dateHandlers";
5 | import diff from "./getDifference";
6 | import "./style.css";
7 |
8 | var dateNow = dateHandle.dateNow();
9 | var daysInYear = dateHandle.daysInYear();
10 | var getDOY = dateHandle.getDOY;
11 |
12 | var mouseDown;
13 | document.body.onmousedown = function() {mouseDown = true}
14 | document.body.onmouseup = function() {mouseDown = false}
15 |
16 | class HeatMapGraph extends Component {
17 | constructor(props) {
18 | super(props);
19 |
20 | this.state = {
21 | showDate: this.props.showDate || false,
22 | highAcc: this.props.highAccuracy || false,
23 | showBorder: this.props.showBorder || false,
24 | addGap: this.props.addGap || false,
25 | dates: [],
26 | selectedGroup: [],
27 | months: new Array(53).fill("blank")
28 | }
29 | }
30 |
31 | componentDidMount() {
32 | if (this.props.setYear !== undefined) {
33 | dateNow.setFullYear(this.props.setYear);
34 | }
35 |
36 | var data = this.props.data || [];
37 | var calendar = new Array(daysInYear).fill({});
38 | var currentDate = new Date(dateNow.getTime());
39 | var date = new Date(currentDate.getTime());
40 | calendar.forEach((e, i) => {
41 | var day = {i: date.getTime(), date, value: "empty", desc: {total: 0}};
42 | calendar[i] = day;
43 | date = new Date(currentDate.setDate(currentDate.getDate() - 1));
44 | });
45 |
46 | var highestTotal = 0;
47 | data.forEach(s => {
48 | if (s.total === undefined) {
49 | s.total = 1
50 | }
51 | var total = s.total;
52 | if (total >= highestTotal) {
53 | highestTotal = total;
54 | }
55 | });
56 |
57 | if (0 in data) {
58 | if (typeof data[0].date === "string" || typeof data[0].date === "number") {
59 | data.forEach(e => {
60 | e.date = new Date(e.date);
61 | })
62 | }
63 | }
64 |
65 | if (this.props.keyNavigation) {
66 | document.onkeydown = this.checkKey.bind(this);
67 | }
68 |
69 | this.setState({calendar: calendar.reverse(), highestTotal, data});
70 | }
71 |
72 | checkKey(e) {
73 | e = e || window.event;
74 | switch (e.keyCode) {
75 | case 27: e.preventDefault(); this.resetDates(); break;
76 | case 37: e.preventDefault(); this.moveSelection(-7, e.shiftKey); break;
77 | case 38: e.preventDefault(); this.moveSelection(-1, e.shiftKey); break;
78 | case 39: e.preventDefault(); this.moveSelection(7, e.shiftKey); break;
79 | case 40: e.preventDefault(); this.moveSelection(1, e.shiftKey); break;
80 | default:;
81 | }
82 | }
83 |
84 | moveSelection(value, shiftKey) {
85 | var orignalDates = this.state.dates.slice();
86 | var dates = orignalDates.map(date => {
87 | var newDate = new Date(date);
88 | return new Date(newDate.setDate(newDate.getDate() + (value)));
89 | });
90 | if (shiftKey) {
91 | dates = dates.concat(orignalDates);
92 | }
93 | this.setState({dates});
94 | if (this.props.receiveDates !== undefined) {
95 | this.props.receiveDates(dates);
96 | }
97 | }
98 |
99 | updateUISelected(selected, wait) {
100 | function run() {
101 | new Array(selected.length).fill("blank").forEach((item, i) => selected[i].lastChild.classList.toggle("selected"));
102 | } run();
103 | setTimeout(function() {
104 | run();
105 | }, wait);
106 | }
107 |
108 | setSelected(grab, number, year) {
109 | var selected = document.getElementsByClassName( grab + number + (year !== undefined ? year : "") );
110 | this.updateUISelected(selected, 200);
111 | var dates = this.state.calendar.slice().filter(day => grab === "d" ? day.date.getDay() === number : day.date.getMonth() === number);
112 | this.returnDates(dates);
113 | }
114 |
115 | selectGroup(e, day) {
116 | var selected = e.currentTarget;
117 | var selectedGroup = this.state.selectedGroup.slice();
118 | if (mouseDown) {
119 | this.updateUISelected([selected], 600);
120 | selectedGroup.push(day);
121 | this.setState({selectedGroup});
122 | }
123 | }
124 |
125 | submitSelectGroup() {
126 | var selectedGroup = this.state.selectedGroup.slice();
127 | this.returnDates(Array.from(new Set(selectedGroup)));
128 | this.setState({selectedGroup: []});
129 | }
130 |
131 | returnDates(newDates) {
132 | var dates = this.state.dates.slice();
133 |
134 | if (!Array.isArray(newDates)) {
135 | newDates = new Array(newDates);
136 | }
137 |
138 | var removeDates = [];
139 | var allowDates = [];
140 |
141 | newDates.forEach(d => {
142 | var allow = true;
143 | dates.forEach(date => {
144 | if (date === d.date) {
145 | allow = false;
146 | }
147 | });
148 | allow ? allowDates.push(d.date) : removeDates.push(d.date);
149 | });
150 |
151 | 0 in allowDates ? allowDates.forEach(date => dates.push(date)) : dates = diff(dates, removeDates);
152 |
153 | setTimeout(function() {
154 | if (this.props.receiveDates !== undefined) {
155 | this.setState({dates});
156 | this.props.receiveDates(dates);
157 | } else {
158 | this.setState({dates: allowDates});
159 | }
160 | }.bind(this), 10);
161 | }
162 |
163 | resetDates() {
164 | this.setState({dates:[]});
165 | if (this.props.receiveDates !== undefined) {
166 | this.props.receiveDates([]);
167 | }
168 | }
169 |
170 | render() {
171 | var calendar = this.state.calendar;
172 | var highestTotal = this.state.highestTotal;
173 | var weekdayLabels = this.props.weekdayLabels || ["", "M", "", "W", "", "F", ""];
174 |
175 | if (calendar !== undefined) {
176 | this.state.data.forEach(s => {
177 | var day = getDOY(s.date) + (daysInYear) - getDOY(dateNow) - 1;
178 | if (day >= daysInYear) {
179 | day = day - daysInYear;
180 | }
181 | if (s.total === undefined) {
182 | s.total = 1
183 | }
184 | var opacity = s.total / highestTotal;
185 | var bgColor = "colorScale0";
186 | var decimal = Math.ceil((s.total / highestTotal) / 0.25) * 0.25;
187 | switch (decimal) {
188 | case 0: bgColor = "colorScale0"; break;
189 | case 0.25: bgColor = "colorScale1"; break;
190 | case 0.50: bgColor = "colorScale2"; break;
191 | case 0.75: bgColor = "colorScale3"; break;
192 | case 1: bgColor = "colorScale4"; break;
193 | default: bgColor = "colorScale0"; break;
194 | }
195 | if (calendar[day].date.getFullYear() === s.date.getFullYear()) {
196 | calendar[day] = {i: calendar[day].i, date: calendar[day].date, value: ("most" + Math.round(decimal)), desc: {label: s.name, total: s.total}, bgColor, opacity};
197 | }
198 | });
199 | }
200 |
201 | const oneYearAgo = new Date(dateNow);
202 | oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
203 | var weekdayBalancer = new Array(oneYearAgo.getDay() + 1).fill("blank");
204 | if (weekdayBalancer.length === 7) {
205 | weekdayBalancer = [];
206 | }
207 |
208 | var months = this.state.months.slice();
209 |
210 | var blankLocations = [];
211 |
212 | if (calendar !== undefined) {
213 | var weekI = 0;
214 | var heatMap = calendar.map((day, i) => {
215 | var weekday = day.date.getDay();
216 | var dayI = day.date.getDate();
217 | var monthI = day.date.getMonth();
218 | var year = day.date.getFullYear();
219 |
220 | if (dayI === 1) {
221 | blankLocations.push(i + (blankLocations.length * 7));
222 | if (this.state.addGap) {
223 | months.push("blank");
224 | }
225 | }
226 |
227 | if (weekday === 0) {
228 | weekI++
229 | if (dayI < 8) {
230 | if (this.state.addGap) {
231 | months[weekI + blankLocations.length] = {month: monthI, year}
232 | } else {
233 | months[weekI] = {month: monthI, year}
234 | }
235 | }
236 | }
237 |
238 | var selected = false;
239 | this.state.dates.forEach(date => {
240 | if (getDOY(date) === getDOY(day.date)) {
241 | selected = true;
242 | }
243 | });
244 |
245 | var border = ((dateHandle.daysInMonth(monthI) - dayI < 7 && dateHandle.daysInMonth(monthI) - dayI >= 0) ? " last7" : "") + (dateHandle.daysInMonth(monthI) === dayI && weekday !== 6 ? " lastDay" : "");
246 |
247 | var _onClick = this.props.onClick || function doNothing() {};
248 |
249 | return this.selectGroup(e, day)}
252 | onMouseDown={(e) => this.selectGroup(e, day)}
253 | onMouseUp={() => this.submitSelectGroup()}
254 | onClick={(e) => _onClick(e, day)}
255 | className={"heatMapGridItem " + day.value + " m" + monthI + year + " d" + weekday + (dayI === 1 ? " firstOfMonth" : "") + (this.state.showBorder ? border : "")}
256 | style={{fontSize: this.state.showDate ? "inherit" : 0}}
257 | >
258 |
259 | {dayI}
260 |
261 | {this.props.showTooltip &&
262 | {day.desc.label !== undefined && {day.desc.label}: {day.desc.total}
}
263 | {findMonthString(monthI, true)} {dayI} {year}
264 | }
265 |
266 |
267 | });
268 | }
269 |
270 | if (this.state.addGap) {
271 | blankLocations.forEach(i => {
272 | var arr = new Array(7).fill("");
273 | arr.forEach((e, ii) => {
274 | heatMap.splice(i, 0,
288 |
289 |
{/*spacer*/}
290 | {months !== undefined && months.map((month, i) =>
this.setSelected("m", month.month, month.year)} className={this.state.addGap ? "heatMapMonthShrink" : "heatMapMonth"}>{findMonthString(month.month, true)}
)}
291 |
292 |
this.submitSelectGroup() : ""}>
293 | {weekdayLabels.map((dayL, i) =>
this.setSelected("d", i)} className="heatMapGridItem legend">{dayL}
)}
294 | {weekdayBalancer.map((a, i) =>
)}
295 | {heatMap}
296 |
297 | {this.state.highAcc ? heatMapScale.gradient() : heatMapScale.box(highestTotal)}
298 |
299 | }
300 | };
301 |
302 | export default HeatMapGraph;
303 |
--------------------------------------------------------------------------------
/src/monthToString.js:
--------------------------------------------------------------------------------
1 | function findMonthString(month, short) {
2 | switch (month) {
3 | case 0: return short ? "Jan" : "January";
4 | case 1: return short ? "Feb" : "February";
5 | case 2: return short ? "Mar" : "March";
6 | case 3: return short ? "Apr" : "April";
7 | case 4: return short ? "May" : "May";
8 | case 5: return short ? "Jun" : "June";
9 | case 6: return short ? "Jul" : "July";
10 | case 7: return short ? "Aug" : "August";
11 | case 8: return short ? "Sep" : "September";
12 | case 9: return short ? "Oct" : "October";
13 | case 10: return short ? "Nov" : "November";
14 | case 11: return short ? "Dec" : "December";
15 | default: ;
16 | }
17 | }
18 |
19 | export default findMonthString;
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | .heatMapGrid {
2 | width: 70px;
3 | height: 70px;
4 | display: grid;
5 | grid-gap: 0px;
6 | grid-template-columns: repeat(7, 0fr);
7 | margin-bottom: 40px;
8 | margin-top: 10px;
9 | margin-left: 25px;
10 | transform: rotate(-90deg) scaleX(-1);
11 | font-size: 8.1px;
12 | }
13 |
14 | .heatMapGridItem {
15 | width: 13px;
16 | height: 13px;
17 | border: 1px solid #ffffff;
18 | transform: rotate(-90deg) scaleX(-1);
19 | transition: font-size .2s ease-in-out, box-shadow .2s ease-out;
20 | text-align: center;
21 | border-radius: 1px;
22 | }
23 |
24 | .heatMapGridItem.shrink {
25 | width: 11px!important;
26 | height: 11px!important;
27 | }
28 |
29 | .heatMapGridItem.empty {
30 | background-color: #ddd;
31 | color: #000;
32 | }
33 |
34 | .heatMapGridItem.empty:hover .heatMapGridItemBackground {
35 | box-shadow: inset 0 0 0 1px #ddd, inset 0 0 0 3px #fff;
36 | opacity: 1;
37 | }
38 |
39 | .heatMapGridItem.most1 {
40 | color: #f0f0f0;
41 | cursor: pointer;
42 | }
43 |
44 | .heatMapGridItem.most0 {
45 | cursor: pointer;
46 | }
47 |
48 | .heatMapGridItem.most1:hover .heatMapGridItemBackground, .heatMapGridItem.most0:hover .heatMapGridItemBackground {
49 | box-shadow: inset 0 0 0 1px #02a5a5, inset 0 0 0 3px #fff;
50 | }
51 |
52 | .heatMapGridItem.legend {
53 | background-color: #fff;
54 | color: #4f5858;
55 | font-weight: 900;
56 | }
57 |
58 | .heatMapGridItem.lastYear {
59 | background-color: #ddd;
60 | }
61 |
62 | .heatMapGridItem.lastYear:before {
63 | content: "|";
64 | transform: rotate(45deg);
65 | position: absolute;
66 | left: 2px;
67 | top: -10px;
68 | font-size: 22px;
69 | color: #ccc;
70 | }
71 |
72 | .heatMapGridItem.last7 {
73 | border-right: 1px solid black;
74 | }
75 |
76 | .heatMapGridItem.lastDay {
77 | border-right: 1px solid black;
78 | border-bottom: 1px solid black;
79 | }
80 |
81 |
82 | .heatMapGridItemBackground.selected {
83 | box-shadow: inset 0 0 0 1px #02a5a5, inset 0 0 0 3px #fff;
84 | }
85 |
86 | .heatMapGridItemBackground.selected:before {
87 | position: absolute;
88 | content:"a";
89 | top:0;
90 | left:0;
91 | width:100%;
92 | height:100%;
93 | opacity:1;
94 | background-color: #4f585859;
95 | }
96 |
97 | .heatMapGridItemBackground {
98 | background-color: #ddd;
99 | width: 100%;
100 | height: 100%;
101 | position: absolute;
102 | top: 0px;
103 | z-index: -1;
104 | transition: background-color .2s ease-in-out, box-shadow .2s ease-out;
105 | }
106 |
107 | .colorScale0{background-color: #ddd;}
108 | .colorScale1{background-color: rgb(002, 184, 184);}
109 | .colorScale2{background-color: rgb(002, 146, 146);}
110 | .colorScale3{background-color: rgb(001, 107, 107);}
111 | .colorScale4{background-color: rgb(001, 068, 068);}
112 |
113 | .heatMapGridItemTooltip {
114 | font-size: 12px;
115 | width: 100px;
116 | padding: 2px;
117 | background-color: #4f5858;
118 | color: #fff;
119 | position: absolute;
120 | display: none;
121 | right: 2px;
122 | bottom: 20px;
123 | border-radius: 3px;
124 | }
125 |
126 | span.heatMapGridItemTooltip::after {
127 | content: "";
128 | background-color: #4f5858;
129 | position: absolute;
130 | right: 8px;
131 | bottom: -4px;
132 | width: 12px;
133 | height: 12px;
134 | z-index: -1;
135 | transform: skew(45deg) rotate(34deg);
136 | }
137 |
138 | .heatMapGridItem:hover .heatMapGridItemTooltip {
139 | display: block;
140 | }
141 |
142 | .heatMapMonths {
143 | font-size: 10px;
144 | display: grid;
145 | width: 10px;
146 | height: 10px;
147 | transform: rotate(-90deg) scaleX(-1);
148 | grid-gap: 2px;
149 | grid-template-columns: repeat(1, 0fr);
150 | grid-template-rows: repeat(52, 0fr);
151 | margin-top: 10px;
152 | margin-left: 25px;
153 | }
154 |
155 | .heatMapMonth {
156 | width: 13px;
157 | height: 13px;
158 | transform: rotate(-90deg) scaleX(-1);
159 | }
160 |
161 | .heatMapMonthShrink {
162 | width: 11px;
163 | height: 11px;
164 | transform: rotate(-90deg) scaleX(-1);
165 | }
166 |
167 | .heatMapScale {
168 | display: flex;
169 | justify-content: center;
170 | margin: 10px;
171 | width: 100%;
172 | }
173 |
174 | .heatMapGridItem.scale {
175 | margin: 1px;
176 | transform: rotate(0deg) scaleX(1);
177 | }
178 |
179 | .heatMapGridItemScaleGradient {
180 | width: 200px;
181 | height: 13px;
182 | margin: 1px;
183 | background: linear-gradient(to right, rgba(221,221,221,1) 0%, rgba(2,184,184,1) 25%, rgba(3,145,145,1) 50%, rgba(1,107,107,1) 75%, rgba(1,68,68,1) 100%);
184 | }
185 |
186 | .heatMapOptions {
187 | padding: 10px;
188 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const pak = require('./package.json');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | const nodeEnv = process.env.NODE_ENV || 'development';
7 |
8 | const webpackConfig = {
9 | context: __dirname,
10 | entry: {
11 | 'heatmap-calendar-react': [
12 | path.resolve(__dirname, 'src', 'index.jsx'),
13 | ],
14 | },
15 | output: {
16 | path: path.resolve(__dirname),
17 | filename: 'index.js',
18 | library: 'HeatMapGraph',
19 | libraryTarget: 'umd',
20 | },
21 | resolve: {
22 | extensions: ['.js', '.jsx'],
23 | modules: ['node_modules'],
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.jsx?$/,
29 | exclude: /(node_modules)/,
30 | use: [{
31 | loader: 'babel-loader',
32 | }],
33 | },
34 | {
35 | test: /\.*css$/,
36 | exclude: /(node_modules)/,
37 | use: ExtractTextPlugin.extract({
38 | fallback: 'style-loader',
39 | use: [
40 | 'css-loader'
41 | ]
42 | })
43 | }
44 | ],
45 | },
46 | plugins: [
47 | new webpack.DefinePlugin({
48 | 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
49 | }),
50 | new ExtractTextPlugin({
51 | filename: 'style.css',
52 | }),
53 | ],
54 | };
55 |
56 | if (nodeEnv === 'development') {
57 | webpackConfig.devtool = 'source-map';
58 | webpackConfig.devServer = { contentBase: './demo' };
59 | webpackConfig.entry['heatmap-calendar-react'].unshift('webpack-dev-server/client?http://0.0.0.0:8080/');
60 | webpackConfig.entry['heatmap-calendar-react'].push(path.resolve(__dirname, 'demo', 'demo.jsx'));
61 | webpackConfig.output.publicPath = '/';
62 | }
63 |
64 | if (nodeEnv === 'demo') {
65 | webpackConfig.entry['heatmap-calendar-react'].push(path.resolve(__dirname, 'demo', 'demo.jsx'));
66 | webpackConfig.output.path = path.resolve(__dirname, 'demo');
67 | }
68 |
69 | if (nodeEnv === 'development' || nodeEnv === 'demo') {
70 | webpackConfig.plugins.push(new webpack.DefinePlugin({
71 | COMPONENT_NAME: JSON.stringify(pak.name),
72 | COMPONENT_VERSION: JSON.stringify(pak.version),
73 | COMPONENT_DESCRIPTION: JSON.stringify(pak.description),
74 | }));
75 | }
76 |
77 | if (nodeEnv === 'production') {
78 | webpackConfig.externals = {
79 | react: {
80 | root: 'React',
81 | commonjs2: 'react',
82 | commonjs: 'react',
83 | amd: 'react',
84 | },
85 | };
86 | webpackConfig.output.path = path.resolve(__dirname, 'build');
87 | webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({
88 | compress: { warnings: false },
89 | sourceMap: false,
90 | }));
91 | }
92 |
93 | module.exports = webpackConfig;
--------------------------------------------------------------------------------