activity-grid
Version:
A customizable activity grid component similar to GitHub's contribution graph
126 lines (112 loc) • 12.5 kB
JavaScript
(function(o,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(o=typeof globalThis<"u"?globalThis:o||self,g(o.ActivityGrid={}))})(this,function(o){"use strict";function g(s){return t=>{customElements.define(s,t)}}function c(s={type:String}){return function(t,e){const a=t.constructor;a.observedAttributes||(a.observedAttributes=[]);const r=s.attribute||e.toLowerCase();a.observedAttributes.includes(r)||a.observedAttributes.push(r);const i=Symbol(e);Object.defineProperty(t,e,{get(){return this[i]},set(n){const p=this[i];s.type===Boolean?this[i]=n===""||n==="true"||n===!0:this[i]=n,this.requestUpdate&&this.requestUpdate(e,p,this[i])},enumerable:!0,configurable:!0})}}function O(){return function(s,t){const e=Symbol(t);Object.defineProperty(s,t,{get(){return this[e]},set(a){const r=this[e];this[e]=a,this.requestUpdate&&this.requestUpdate(t,r,a)},enumerable:!0,configurable:!0})}}function C(s){if(!s||typeof s!="object"||!("date"in s)||!("count"in s))return!1;const t=s,e=new Date(t.date);return!(isNaN(e.getTime())||typeof t.count!="number"||t.count<0)}const m={default:["#ebedf0","#9be9a8","#40c463","#30a14e","#216e39"],green:["#ebedf0","#9be9a8","#40c463","#30a14e","#216e39"],red:["#ebedf0","#ffcdd2","#ef5350","#e53935","#b71c1c"],blue:["#ebedf0","#bbdefb","#64b5f6","#1e88e5","#0d47a1"],yellow:["#ebedf0","#fff9c4","#ffee58","#fdd835","#f57f17"],purple:["#ebedf0","#e1bee7","#ab47bc","#8e24aa","#4a148c"]},$=s=>s in m&&s!=="default",L=`
<style>
:host {
display: inline-block;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
/* Dark mode styles */
:host([dark-mode]) {
color: #c9d1d9;
background-color: transparent;
}
.container {
display: inline-grid;
grid-template-rows: auto 1fr;
}
.months {
display: flex;
padding-left: 32px;
font-size: 12px;
color: #767676;
height: 20px;
}
:host([dark-mode]) .months {
color: #8b949e;
}
.months-spacer {
width: 30px;
}
.months-container {
display: flex;
justify-content: space-between;
flex: 1;
}
.months span {
padding: 0 4px;
}
.grid-wrapper {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px;
}
.weekdays {
display: grid;
grid-template-rows: repeat(7, 1fr);
gap: 2px;
text-align: right;
padding-left: 6px;
padding-right: 2px;
font-size: 12px;
color: #767676;
margin-top: -1px;
height: calc(7 * 10px + 6 * 2px);
}
:host([dark-mode]) .weekdays {
color: #8b949e;
}
.weekdays div {
height: 10px;
line-height: 10px;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-template-rows: repeat(7, 1fr);
gap: 2px;
}
.cell {
width: 10px;
height: 10px;
border-radius: 2px;
}
</style>
`,x=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],A={mondayStart:["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],sundayStart:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},f={light:"#ebedf0",dark:"#161b22"},b=s=>`${s.getFullYear()}-${(s.getMonth()+1).toString().padStart(2,"0")}-${s.getDate().toString().padStart(2,"0")}`;class F{createMonthLabels(t,e){const a=[],r=new Date(t);for(r.setDate(1);r<=e;)a.push(x[r.getMonth()]),r.setMonth(r.getMonth()+1);return`
<div class="months-container">
${a.map(i=>`<span>${i}</span>`).join("")}
</div>
`}createWeekLabels(t,e=[0,1,2,3,4,5,6]){return`
<div class="weekdays">
${(t?A.mondayStart:A.sundayStart).map((r,i)=>`<div>${e.includes(i)?r:""}</div>`).join(`
`)}
</div>
`}getWeeksBetweenDates(t,e){const r=Math.abs(e.getTime()-t.getTime());return Math.ceil(r/6048e5)}defaultTitleFormatter(t,e,a){const r=e===1?"activity":"activities";return`${t.toDateString()}: ${e} ${r}`}createGridCells(t,e,a,r,i,n,p,y){let u="";const v=n?5:7,k=new Date(a),S=new Date(e);S.setHours(0,0,0,0);let W=n?5-a.getDay():7-a.getDay();p&&(W+=1),k.setDate(k.getDate()+W);const T=new Date(e),G=p?(e.getDay()||7)-1:e.getDay();G!==0&&T.setDate(e.getDate()-G);const N=this.getWeeksBetweenDates(k,e);for(let D=0;D<v;D++){const j=D>4;if(!(n&&j))for(let _=0;_<N;_++){const l=new Date(T);l.setDate(l.getDate()+D+_*7),l.setHours(0,0,0,0);const w=b(l),h=t[w];if(l<S)u+=`
<div class="cell"
style="background-color: transparent">
</div>`;else if(l<=a)if(h){const M=y?y(l,h.count,h.id):this.defaultTitleFormatter(l,h.count,h.id);u+=`
<div class="cell"
style="background-color: ${r[h.level]||i}"
title="${M}"
data-date="${w}"
data-count="${h.count}"
${h.id?`cell-id="${h.id}"`:""}>
</div>`}else{const M=y?y(l,0):this.defaultTitleFormatter(l,0);u+=`
<div class="cell"
style="background-color: ${i}"
title="${M}"
data-date="${w}"
data-count="0">
</div>`}else l<=k&&(u+=`
<div class="cell"
style="background-color: transparent">
</div>`)}}return u}render(t,e,a,r){const i=new Date(a);let n=r.skipWeekends?5-a.getDay():7-a.getDay();r.startWeekOnMonday&&(n+=1),i.setDate(i.getDate()+n);const p=this.getWeeksBetweenDates(i,e),y=this.createMonthLabels(e,a),u=this.createWeekLabels(r.startWeekOnMonday,r.startWeekOnMonday?[0,2,4]:[1,3,5]),v=this.createGridCells(t,e,a,r.colors,r.emptyColor,r.skipWeekends,r.startWeekOnMonday,r.titleFormatter);return{html:`
<div class="container">
<div class="months">${y}</div>
<div class="grid-wrapper">
${u}
<div class="grid">
${v}
</div>
</div>
</div>
`,numOfWeeks:p}}}function E(s){return new CustomEvent("cell-click",{detail:s,bubbles:!0,composed:!0})}var U=Object.defineProperty,I=Object.getOwnPropertyDescriptor,d=(s,t,e,a)=>{for(var r=a>1?void 0:a?I(t,e):t,i=s.length-1,n;i>=0;i--)(n=s[i])&&(r=(a?n(t,e,r):n(r))||r);return a&&r&&U(t,e,r),r};o.ActivityGrid=class extends HTMLElement{constructor(){super(),this.gridRenderer=new F,this._data=[],this._colors=m.default,this._colorTheme=null,this._darkMode=!1,this._emptyColor=this._darkMode?f.dark:f.light,this._skipWeekends=!1,this._startWeekOnMonday=!1,this._endDate=new Date,this._titleFormatter=null,this.cells={},this.attachShadow({mode:"open"}),this._startDate=null}set data(t){this.validateActivityData(t)&&(this._data=t,this.updateGrid())}get data(){return this._data}set colors(t){if(!t||!Array.isArray(t)||t.length===0){console.warn("Invalid or empty colors array provided. Using default theme."),this._colors=m.default,this.updateGrid();return}const e=t.filter(a=>!this.validateColor(a));if(e.length>0){console.warn(`Invalid colors found when trying to set color theme: ${e.join(", ")}. Using default theme.`),this._colors=m.default,this.updateGrid();return}this._colorTheme||(this._colors=t,this.updateGrid())}get colors(){return this._colorTheme?m[this._colorTheme]:this._colors}set colorTheme(t){t?$(t)?this._colorTheme=t:(console.warn(`Invalid color theme "${t}". Using default theme.`),this._colorTheme=null):this._colorTheme=null,this.updateGrid()}get colorTheme(){return this._colorTheme||""}set darkMode(t){this._darkMode=t,this.emptyColor=t?f.dark:f.light,this.updateGrid()}get darkMode(){return this._darkMode}set emptyColor(t){if(t===null||!this.validateColor(t)){this._emptyColor=this._darkMode?f.dark:f.light;return}this._emptyColor=t,this.updateGrid()}get emptyColor(){return this._emptyColor}set skipWeekends(t){this._skipWeekends=t,t&&(this.startWeekOnMonday=!0),this.updateGrid()}get skipWeekends(){return this._skipWeekends}set startWeekOnMonday(t){this._startWeekOnMonday=this.skipWeekends?!0:t,this.updateGrid()}get startWeekOnMonday(){return this._startWeekOnMonday}set endDate(t){const e=t?new Date(t):new Date;isNaN(e.getTime())?(console.warn("Invalid end-date provided. Using current date instead."),this._endDate=new Date):this._endDate=e,this.updateGrid()}get endDate(){return b(this._endDate)}set startDate(t){const e=t?new Date(t):null;e&&!isNaN(e.getTime())?e>this._endDate?(console.warn("Start date cannot be after end date. Using one year before end date instead."),this._startDate=this.createDefaultStartDate()):this._startDate=e:this._startDate||(console.warn("Invalid start-date provided. Using one year before end date instead."),this._startDate=this.createDefaultStartDate()),this.isConnected&&this.updateGrid()}get startDate(){return this._startDate?b(this._startDate):b(this.createDefaultStartDate())}set titleFormatter(t){this._titleFormatter=t,this.updateGrid()}get titleFormatter(){return this._titleFormatter}connectedCallback(){this._startDate||(this._startDate=this.createDefaultStartDate()),this.updateGrid()}attributeChangedCallback(t,e,a){const r=t.replace(/-([a-z])/g,i=>i[1].toUpperCase());if(this[r]!==void 0){const i=this[r];if(typeof i=="boolean")this[r]=a!==null;else if(typeof i=="number")this[r]=Number(a);else if(Array.isArray(i))try{this[r]=JSON.parse(a||"[]")}catch(n){console.warn(`Invalid array value for ${t}:`,n)}else this[r]=a}}static get observedAttributes(){return["start-week-on-monday","skip-weekends","data","colors","color-theme","empty-color","max-level","end-date","start-date","dark-mode"]}createDefaultStartDate(){const t=new Date(this._endDate);t.setFullYear(t.getFullYear()-1);const e=this.startWeekOnMonday?(t.getDay()||7)-1:t.getDay();return e!==0&&t.setDate(t.getDate()-e),t}generateGridCells(){const t={};return this.data.forEach(e=>{t[e.date]={date:new Date(e.date),count:e.count,level:this.calculateLevel(e.count),ignore:!1,id:e.id}}),t}validateActivityData(t){if(!t||!Array.isArray(t))return console.warn("Invalid activity data: must be an array. Using empty array instead."),this._data=[],!1;const e=t.filter(a=>!C(a));return e.length>0&&console.warn("Invalid items found in activity data. They will be filtered out:",e),!0}validateColor(t){return CSS.supports("color",t)}calculateLevel(t){if(t===0)return 0;const e=this.colors.length-1,a=Math.max(...this.data.map(r=>r.count));return Math.ceil(t/a*e)}updateGrid(){if(!this.shadowRoot)return;this.cells=this.generateGridCells(),this._startDate||(this._startDate=this.createDefaultStartDate());const{html:t,numOfWeeks:e}=this.gridRenderer.render(this.cells,this._startDate,this._endDate,{colors:this.colors,emptyColor:this.emptyColor,skipWeekends:this.skipWeekends,startWeekOnMonday:this.startWeekOnMonday,titleFormatter:this.titleFormatter});this.style.setProperty("--grid-columns",e.toString()),this.shadowRoot.innerHTML=`${L}${t}`,this.attachEventListeners()}attachEventListeners(){if(!this.shadowRoot)return;this.shadowRoot.querySelectorAll(".cell[data-date]").forEach(e=>{e.addEventListener("click",a=>{const r=e.getAttribute("data-date"),i=parseInt(e.getAttribute("data-count")||"0",10),n=e.getAttribute("cell-id")||void 0;r&&this.dispatchEvent(E({date:r,count:i,id:n}))})})}},d([c({type:Array})],o.ActivityGrid.prototype,"data",1),d([c({type:Array})],o.ActivityGrid.prototype,"colors",1),d([c({type:String,attribute:"color-theme"})],o.ActivityGrid.prototype,"colorTheme",1),d([c({type:Boolean,attribute:"dark-mode"})],o.ActivityGrid.prototype,"darkMode",1),d([c({type:String,attribute:"empty-color"})],o.ActivityGrid.prototype,"emptyColor",1),d([c({type:Boolean})],o.ActivityGrid.prototype,"skipWeekends",1),d([c({type:Boolean})],o.ActivityGrid.prototype,"startWeekOnMonday",1),d([c({type:String,attribute:"end-date"})],o.ActivityGrid.prototype,"endDate",1),d([c({type:String,attribute:"start-date"})],o.ActivityGrid.prototype,"startDate",1),d([O()],o.ActivityGrid.prototype,"cells",2),o.ActivityGrid=d([g("activity-grid")],o.ActivityGrid),Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})});
//# sourceMappingURL=activity-grid.umd.js.map