mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-23 14:34:21 +02:00
User action heatmap (#5131)
* Added basic heatmap data * Added extra case for sqlite * Built basic heatmap into user profile * Get contribution data from api & styling * Fixed lint & added extra group by statements for all database types * generated swagger spec * generated swagger spec * generated swagger spec * fixed swagger spec * fmt * Added tests * Added setting to enable/disable user heatmap * Added locale for loading text * Removed UseTiDB * Updated librejs & moment.js * Fixed import order * Fixed heatmap in postgresql * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: kolaente <konrad@kola-entertainments.de> * Added copyright header * Fixed a bug to show the heatmap for the actual user instead of the currently logged in * Added integration test for heatmaps * Added a heatmap on the dashboard * Fixed timestamp parsing * Hide heatmap on mobile * optimized postgresql group by query * Improved sqlite group by statement
This commit is contained in:
1 parent
f38fce916e
commit
6759237eda
27 files changed
+649
-1
No files matched your search
@@ -193,6 +193,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
|
||||
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
|
||||
- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
|
||||
- `ENABLE_USER_HEATMAP`: **true** Enable this to display the heatmap on users profiles.
|
||||
|
||||
## Webhook (`webhook`)
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.package models
|
||||
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserHeatmap(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
session := loginUser(t, adminUsername)
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/users/%s/heatmap", normalUsername)
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var heatmap []*models.UserHeatmapData
|
||||
DecodeJSON(t, resp, &heatmap)
|
||||
var dummyheatmap []*models.UserHeatmapData
|
||||
dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1540080000, Contributions: 1})
|
||||
|
||||
assert.Equal(t, dummyheatmap, heatmap)
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
act_user_id: 2
|
||||
repo_id: 2
|
||||
is_private: true
|
||||
created_unix: 1540139562
|
||||
|
||||
-
|
||||
id: 2
|
||||
|
||||
@@ -48,6 +48,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
|
||||
setting.RunUser = "runuser"
|
||||
setting.SSH.Port = 3000
|
||||
setting.SSH.Domain = "try.gitea.io"
|
||||
setting.UseSQLite3 = true
|
||||
setting.RepoRootPath, err = ioutil.TempDir(os.TempDir(), "repos")
|
||||
if err != nil {
|
||||
fatalTestError("TempDir: %v\n", err)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.package models
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// UserHeatmapData represents the data needed to create a heatmap
|
||||
type UserHeatmapData struct {
|
||||
Timestamp util.TimeStamp `json:"timestamp"`
|
||||
Contributions int64 `json:"contributions"`
|
||||
}
|
||||
|
||||
// GetUserHeatmapDataByUser returns an array of UserHeatmapData
|
||||
func GetUserHeatmapDataByUser(user *User) (hdata []*UserHeatmapData, err error) {
|
||||
var groupBy string
|
||||
switch {
|
||||
case setting.UseSQLite3:
|
||||
groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))"
|
||||
case setting.UseMySQL:
|
||||
groupBy = "UNIX_TIMESTAMP(DATE_FORMAT(FROM_UNIXTIME(created_unix), '%Y%m%d'))"
|
||||
case setting.UsePostgreSQL:
|
||||
groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))"
|
||||
case setting.UseMSSQL:
|
||||
groupBy = "dateadd(DAY,0, datediff(day,0, dateadd(s, created_unix, '19700101')))"
|
||||
}
|
||||
|
||||
err = x.Select(groupBy+" as timestamp, count(user_id) as contributions").
|
||||
Table("action").
|
||||
Where("user_id = ?", user.ID).
|
||||
And("created_unix > ?", (util.TimeStampNow() - 31536000)).
|
||||
GroupBy("timestamp").
|
||||
OrderBy("timestamp").
|
||||
Find(&hdata)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.package models
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetUserHeatmapDataByUser(t *testing.T) {
|
||||
// Prepare
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Insert some action
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
// get the action for comparison
|
||||
actions, err := GetFeeds(GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
RequestingUserID: user.ID,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the heatmap and compare
|
||||
heatmap, err := GetUserHeatmapDataByUser(user)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(actions), len(heatmap))
|
||||
}
|
||||
@@ -1218,6 +1218,7 @@ var Service struct {
|
||||
DefaultEnableDependencies bool
|
||||
DefaultAllowOnlyContributorsToTrackTime bool
|
||||
NoReplyAddress string
|
||||
EnableUserHeatmap bool
|
||||
|
||||
// OpenID settings
|
||||
EnableOpenIDSignIn bool
|
||||
@@ -1249,6 +1250,7 @@ func newService() {
|
||||
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
|
||||
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
|
||||
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
|
||||
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
|
||||
|
||||
sec = Cfg.Section("openid")
|
||||
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
|
||||
|
||||
@@ -320,6 +320,7 @@ starred = Starred Repositories
|
||||
following = Following
|
||||
follow = Follow
|
||||
unfollow = Unfollow
|
||||
heatmap.loading = Loading Heatmap…
|
||||
|
||||
form.name_reserved = The username '%s' is reserved.
|
||||
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
|
||||
|
||||
@@ -1 +1 @@
|
||||
.tribute-container{box-shadow:0 1px 3px 1px #c7c7c7}.tribute-container ul{background:#fff}.tribute-container li{padding:8px 12px;border-bottom:1px solid #dcdcdc}.tribute-container li img{display:inline-block;vertical-align:middle;width:28px;height:28px;margin-right:5px}.tribute-container li span.fullname{font-weight:400;font-size:.8rem;margin-left:3px}.tribute-container li.highlight,.tribute-container li:hover{background:#2185D0;color:#fff}.emoji{width:1.5em;height:1.5em;display:inline-block;background-size:contain}body{font-family:Lato,"Segoe UI","Microsoft YaHei",Arial,Helvetica,sans-serif!important;background-color:#fff;overflow-y:scroll;-webkit-font-smoothing:antialiased}img{border-radius:3px}table{border-collapse:collapse}.rounded{border-radius:.28571429rem!important}code,pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace}code.raw,pre.raw{padding:7px 12px;margin:10px 0;background-color:#f8f8f8;border:1px solid #ddd;border-radius:3px;font-size:13px;line-height:1.5;overflow:auto}code.wrap,pre.wrap{white-space:pre-wrap;-ms-word-break:break-all;word-break:break-all;overflow-wrap:break-word;word-wrap:break-word}.dont-break-out{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.full.height{padding:0;margin:0 0 -40px 0;min-height:100%}.following.bar{z-index:900;left:0;width:100%;margin:0}.following.bar.light{background-color:#fff;border-bottom:1px solid #DDD;box-shadow:0 2px 3px rgba(0,0,0,.04)}.following.bar .column .menu{margin-top:0}.following.bar .top.menu a.item.brand{padding-left:0}.following.bar .brand .ui.mini.image{width:30px}.following.bar .top.menu .dropdown.item.active,.following.bar .top.menu .dropdown.item:hover,.following.bar .top.menu a.item:hover{background-color:transparent}.following.bar .top.menu a.item:hover{color:rgba(0,0,0,.45)}.following.bar .top.menu .menu{z-index:900}.following.bar .octicon{margin-right:.75em}.following.bar .octicon.fitted{margin-right:0}.following.bar .searchbox{background-color:#f4f4f4!important}.following.bar .searchbox:focus{background-color:#e9e9e9!important}.following.bar .text .octicon{width:16px;text-align:center}@media only screen and (max-width:767px){.following.bar #navbar:not(.shown)>:not(:first-child){display:none}}.right.stackable.menu{margin-left:auto;display:flex;display:-ms-flexbox;-ms-flex-align:inherit;align-items:inherit;-ms-flex-direction:inherit;flex-direction:inherit}.ui.left{float:left}.ui.right{float:right}.ui.button,.ui.menu .item{-moz-user-select:auto;-ms-user-select:auto;-webkit-user-select:auto;user-select:auto}.ui.container.fluid.padded{padding:0 10px 0 10px}.ui.form .ui.button{font-weight:400}.ui.floating.label{z-index:10}.ui.transparent.label{background-color:transparent}.ui.menu,.ui.segment,.ui.vertical.menu{box-shadow:none}.ui .menu:not(.vertical) .item>.button.compact{padding:.58928571em 1.125em}.ui .menu:not(.vertical) .item>.button.small{font-size:.92857143rem}.ui.dropdown .menu>.item>.floating.label{z-index:11}.ui.dropdown .menu .menu>.item>.floating.label{z-index:21}.ui .text.red{color:#d95c5c!important}.ui .text.red a{color:#d95c5c!important}.ui .text.red a:hover{color:#E67777!important}.ui .text.blue{color:#428bca!important}.ui .text.blue a{color:#15c!important}.ui .text.blue a:hover{color:#428bca!important}.ui .text.black{color:#444}.ui .text.black:hover{color:#000}.ui .text.grey{color:#767676!important}.ui .text.grey a{color:#444!important}.ui .text.grey a:hover{color:#000!important}.ui .text.light.grey{color:#888!important}.ui .text.green{color:#6cc644!important}.ui .text.purple{color:#6e5494!important}.ui .text.yellow{color:#FBBD08!important}.ui .text.gold{color:#a1882b!important}.ui .text.left{text-align:left!important}.ui .text.right{text-align:right!important}.ui .text.small{font-size:.75em}.ui .text.normal{font-weight:400}.ui .text.bold{font-weight:700}.ui .text.italic{font-style:italic}.ui .text.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}.ui .text.thin{font-weight:400}.ui .text.middle{vertical-align:middle}.ui .message{text-align:center}.ui .header>i+.content{padding-left:.75rem;vertical-align:middle}.ui .warning.header{background-color:#F9EDBE!important;border-color:#F0C36D}.ui .warning.segment{border-color:#F0C36D}.ui .info.segment{border:1px solid #c5d5dd}.ui .info.segment.top{background-color:#e6f1f6!important}.ui .info.segment.top h3,.ui .info.segment.top h4{margin-top:0}.ui .info.segment.top h3:last-child{margin-top:4px}.ui .info.segment.top>:last-child{margin-bottom:0}.ui .normal.header{font-weight:400}.ui .avatar.image{border-radius:3px}.ui .form .fake{display:none!important}.ui .form .sub.field{margin-left:25px}.ui .sha.label{font-family:Consolas,Menlo,Monaco,"Lucida Console",monospace;font-size:13px;padding:6px 10px 4px 10px;font-weight:400;margin:0 6px}.ui.status.buttons .octicon{margin-right:4px}.ui.inline.delete-button{padding:8px 15px;fLine truncated
|
||||
.tribute-container{box-shadow:0 1px 3px 1px #c7c7c7}.tribute-container ul{background:#fff}.tribute-container li{padding:8px 12px;border-bottom:1px solid #dcdcdc}.tribute-container li img{display:inline-block;vertical-align:middle;width:28px;height:28px;margin-right:5px}.tribute-container li span.fullname{font-weight:400;font-size:.8rem;margin-left:3px}.tribute-container li.highlight,.tribute-container li:hover{background:#2185D0;color:#fff}.emoji{width:1.5em;height:1.5em;display:inline-block;background-size:contain}body{font-family:Lato,"Segoe UI","Microsoft YaHei",Arial,Helvetica,sans-serif!important;background-color:#fff;overflow-y:scroll;-webkit-font-smoothing:antialiased}img{border-radius:3px}table{border-collapse:collapse}.rounded{border-radius:.28571429rem!important}code,pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace}code.raw,pre.raw{padding:7px 12px;margin:10px 0;background-color:#f8f8f8;border:1px solid #ddd;border-radius:3px;font-size:13px;line-height:1.5;overflow:auto}code.wrap,pre.wrap{white-space:pre-wrap;-ms-word-break:break-all;word-break:break-all;overflow-wrap:break-word;word-wrap:break-word}.dont-break-out{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.full.height{padding:0;margin:0 0 -40px 0;min-height:100%}.following.bar{z-index:900;left:0;width:100%;margin:0}.following.bar.light{background-color:#fff;border-bottom:1px solid #DDD;box-shadow:0 2px 3px rgba(0,0,0,.04)}.following.bar .column .menu{margin-top:0}.following.bar .top.menu a.item.brand{padding-left:0}.following.bar .brand .ui.mini.image{width:30px}.following.bar .top.menu .dropdown.item.active,.following.bar .top.menu .dropdown.item:hover,.following.bar .top.menu a.item:hover{background-color:transparent}.following.bar .top.menu a.item:hover{color:rgba(0,0,0,.45)}.following.bar .top.menu .menu{z-index:900}.following.bar .octicon{margin-right:.75em}.following.bar .octicon.fitted{margin-right:0}.following.bar .searchbox{background-color:#f4f4f4!important}.following.bar .searchbox:focus{background-color:#e9e9e9!important}.following.bar .text .octicon{width:16px;text-align:center}@media only screen and (max-width:767px){.following.bar #navbar:not(.shown)>:not(:first-child){display:none}}.right.stackable.menu{margin-left:auto;display:flex;display:-ms-flexbox;-ms-flex-align:inherit;align-items:inherit;-ms-flex-direction:inherit;flex-direction:inherit}.ui.left{float:left}.ui.right{float:right}.ui.button,.ui.menu .item{-moz-user-select:auto;-ms-user-select:auto;-webkit-user-select:auto;user-select:auto}.ui.container.fluid.padded{padding:0 10px 0 10px}.ui.form .ui.button{font-weight:400}.ui.floating.label{z-index:10}.ui.transparent.label{background-color:transparent}.ui.menu,.ui.segment,.ui.vertical.menu{box-shadow:none}.ui .menu:not(.vertical) .item>.button.compact{padding:.58928571em 1.125em}.ui .menu:not(.vertical) .item>.button.small{font-size:.92857143rem}.ui.dropdown .menu>.item>.floating.label{z-index:11}.ui.dropdown .menu .menu>.item>.floating.label{z-index:21}.ui .text.red{color:#d95c5c!important}.ui .text.red a{color:#d95c5c!important}.ui .text.red a:hover{color:#E67777!important}.ui .text.blue{color:#428bca!important}.ui .text.blue a{color:#15c!important}.ui .text.blue a:hover{color:#428bca!important}.ui .text.black{color:#444}.ui .text.black:hover{color:#000}.ui .text.grey{color:#767676!important}.ui .text.grey a{color:#444!important}.ui .text.grey a:hover{color:#000!important}.ui .text.light.grey{color:#888!important}.ui .text.green{color:#6cc644!important}.ui .text.purple{color:#6e5494!important}.ui .text.yellow{color:#FBBD08!important}.ui .text.gold{color:#a1882b!important}.ui .text.left{text-align:left!important}.ui .text.right{text-align:right!important}.ui .text.small{font-size:.75em}.ui .text.normal{font-weight:400}.ui .text.bold{font-weight:700}.ui .text.italic{font-style:italic}.ui .text.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}.ui .text.thin{font-weight:400}.ui .text.middle{vertical-align:middle}.ui .message{text-align:center}.ui .header>i+.content{padding-left:.75rem;vertical-align:middle}.ui .warning.header{background-color:#F9EDBE!important;border-color:#F0C36D}.ui .warning.segment{border-color:#F0C36D}.ui .info.segment{border:1px solid #c5d5dd}.ui .info.segment.top{background-color:#e6f1f6!important}.ui .info.segment.top h3,.ui .info.segment.top h4{margin-top:0}.ui .info.segment.top h3:last-child{margin-top:4px}.ui .info.segment.top>:last-child{margin-bottom:0}.ui .normal.header{font-weight:400}.ui .avatar.image{border-radius:3px}.ui .form .fake{display:none!important}.ui .form .sub.field{margin-left:25px}.ui .sha.label{font-family:Consolas,Menlo,Monaco,"Lucida Console",monospace;font-size:13px;padding:6px 10px 4px 10px;font-weight:400;margin:0 6px}.ui.status.buttons .octicon{margin-right:4px}.ui.inline.delete-button{padding:8px 15px;fLine truncated
|
||||
@@ -588,3 +588,20 @@ footer {
|
||||
border-bottom-width: 0 !important;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
#user-heatmap{
|
||||
width: 107%; // Fixes newest contributions not showing
|
||||
text-align: center;
|
||||
margin: 40px 0 30px;
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: inherit;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
& {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,10 @@
|
||||
.ui.repository.list {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
#loading-heatmap{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&.followers {
|
||||
|
||||
Vendored
+9
@@ -58,3 +58,12 @@ Version: 4.3.0
|
||||
|
||||
File(s): /vendor/assets/swagger-ui/
|
||||
Version: 3.0.4
|
||||
|
||||
File(s): /vendor/plugins/d3/
|
||||
Version: 4.13.0
|
||||
|
||||
File(s): /vendor/plugins/calendar-heatmap/
|
||||
Version: 337b431
|
||||
|
||||
File(s): /vendor/plugins/moment/
|
||||
Version: 2.22.2
|
||||
Vendored
+15
@@ -135,6 +135,21 @@
|
||||
<td><a href="https://github.com/swagger-api/swagger-ui/blob/master/LICENSE">Apache-2.0</a></td>
|
||||
<td><a href="https://github.com/swagger-api/swagger-ui/archive/v3.0.4.tar.gz">swagger-ui-v3.0.4.tar.gz</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="./plugins/d3/">d3</a></td>
|
||||
<td><a href="https://github.com/d3/d3/blob/master/LICENSE">BSD 3-Clause</a></td>
|
||||
<td><a href="https://github.com/d3/d3/releases/download/v4.13.0/d3.zip">d3.zip</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="./plugins/calendar-heatmap/">calendar-heatmap</a></td>
|
||||
<td><a href="https://github.com/DKirwan/calendar-heatmap/blob/master/LICENSE">MIT</a></td>
|
||||
<td><a href="https://github.com/DKirwan/calendar-heatmap/archive/master.zip">337b431.zip</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="./plugins/moment/">moment.js</a></td>
|
||||
<td><a href="https://github.com/moment/moment/blob/develop/LICENSE">MIT</a></td>
|
||||
<td><a href="https://github.com/moment/moment/archive/2.22.2.tar.gz">0.4.1.tar.gz</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
text.month-name,
|
||||
text.calendar-heatmap-legend-text,
|
||||
text.day-initial {
|
||||
font-size: 10px;
|
||||
fill: inherit;
|
||||
font-family: Helvetica, arial, 'Open Sans', sans-serif;
|
||||
}
|
||||
rect.day-cell:hover {
|
||||
stroke: #555555;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.day-cell-tooltip {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
padding: 5px 9px;
|
||||
color: #bbbbbb;
|
||||
font-size: 12px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
.day-cell-tooltip > span {
|
||||
font-family: Helvetica, arial, 'Open Sans', sans-serif
|
||||
}
|
||||
.calendar-heatmap {
|
||||
box-sizing: initial;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// https://github.com/DKirwan/calendar-heatmap
|
||||
|
||||
function calendarHeatmap() {
|
||||
// defaults
|
||||
var width = 750;
|
||||
var height = 110;
|
||||
var legendWidth = 150;
|
||||
var selector = 'body';
|
||||
var SQUARE_LENGTH = 11;
|
||||
var SQUARE_PADDING = 2;
|
||||
var MONTH_LABEL_PADDING = 6;
|
||||
var now = moment().endOf('day').toDate();
|
||||
var yearAgo = moment().startOf('day').subtract(1, 'year').toDate();
|
||||
var startDate = null;
|
||||
var counterMap= {};
|
||||
var data = [];
|
||||
var max = null;
|
||||
var colorRange = ['#D8E6E7', '#218380'];
|
||||
var tooltipEnabled = true;
|
||||
var tooltipUnit = 'contribution';
|
||||
var legendEnabled = true;
|
||||
var onClick = null;
|
||||
var weekStart = 1; //0 for Sunday, 1 for Monday
|
||||
var locale = {
|
||||
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
days: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
|
||||
No: 'No',
|
||||
on: 'on',
|
||||
Less: 'Less',
|
||||
More: 'More'
|
||||
};
|
||||
var v = Number(d3.version.split('.')[0]);
|
||||
|
||||
// setters and getters
|
||||
chart.data = function (value) {
|
||||
if (!arguments.length) { return data; }
|
||||
data = value;
|
||||
|
||||
counterMap= {};
|
||||
|
||||
data.forEach(function (element, index) {
|
||||
var key= moment(element.date).format( 'YYYY-MM-DD' );
|
||||
var counter= counterMap[key] || 0;
|
||||
counterMap[key]= counter + element.count;
|
||||
});
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.max = function (value) {
|
||||
if (!arguments.length) { return max; }
|
||||
max = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.selector = function (value) {
|
||||
if (!arguments.length) { return selector; }
|
||||
selector = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.startDate = function (value) {
|
||||
if (!arguments.length) { return startDate; }
|
||||
yearAgo = value;
|
||||
now = moment(value).endOf('day').add(1, 'year').toDate();
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.colorRange = function (value) {
|
||||
if (!arguments.length) { return colorRange; }
|
||||
colorRange = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.tooltipEnabled = function (value) {
|
||||
if (!arguments.length) { return tooltipEnabled; }
|
||||
tooltipEnabled = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.tooltipUnit = function (value) {
|
||||
if (!arguments.length) { return tooltipUnit; }
|
||||
tooltipUnit = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.legendEnabled = function (value) {
|
||||
if (!arguments.length) { return legendEnabled; }
|
||||
legendEnabled = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.onClick = function (value) {
|
||||
if (!arguments.length) { return onClick(); }
|
||||
onClick = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
chart.locale = function (value) {
|
||||
if (!arguments.length) { return locale; }
|
||||
locale = value;
|
||||
return chart;
|
||||
};
|
||||
|
||||
function chart() {
|
||||
|
||||
d3.select(chart.selector()).selectAll('svg.calendar-heatmap').remove(); // remove the existing chart, if it exists
|
||||
|
||||
var dateRange = ((d3.time && d3.time.days) || d3.timeDays)(yearAgo, now); // generates an array of date objects within the specified range
|
||||
var monthRange = ((d3.time && d3.time.months) || d3.timeMonths)(moment(yearAgo).startOf('month').toDate(), now); // it ignores the first month if the 1st date is after the start of the month
|
||||
var firstDate = moment(dateRange[0]);
|
||||
if (chart.data().length == 0) {
|
||||
max = 0;
|
||||
} else if (max === null) {
|
||||
max = d3.max(chart.data(), function (d) { return d.count; }); // max data value
|
||||
}
|
||||
|
||||
// color range
|
||||
var color = ((d3.scale && d3.scale.linear) || d3.scaleLinear)()
|
||||
.range(chart.colorRange())
|
||||
.domain([0, max]);
|
||||
|
||||
var tooltip;
|
||||
var dayRects;
|
||||
|
||||
drawChart();
|
||||
|
||||
function drawChart() {
|
||||
var svg = d3.select(chart.selector())
|
||||
.style('position', 'relative')
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('class', 'calendar-heatmap')
|
||||
.attr('height', height)
|
||||
.style('padding', '36px');
|
||||
|
||||
dayRects = svg.selectAll('.day-cell')
|
||||
.data(dateRange); // array of days for the last yr
|
||||
|
||||
var enterSelection = dayRects.enter().append('rect')
|
||||
.attr('class', 'day-cell')
|
||||
.attr('width', SQUARE_LENGTH)
|
||||
.attr('height', SQUARE_LENGTH)
|
||||
.attr('fill', function(d) { return color(countForDate(d)); })
|
||||
.attr('x', function (d, i) {
|
||||
var cellDate = moment(d);
|
||||
var result = cellDate.week() - firstDate.week() + (firstDate.weeksInYear() * (cellDate.weekYear() - firstDate.weekYear()));
|
||||
return result * (SQUARE_LENGTH + SQUARE_PADDING);
|
||||
})
|
||||
.attr('y', function (d, i) {
|
||||
return MONTH_LABEL_PADDING + formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING);
|
||||
});
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
(v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('click', function(d) {
|
||||
var count = countForDate(d);
|
||||
onClick({ date: d, count: count});
|
||||
});
|
||||
}
|
||||
|
||||
if (chart.tooltipEnabled()) {
|
||||
(v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('mouseover', function(d, i) {
|
||||
tooltip = d3.select(chart.selector())
|
||||
.append('div')
|
||||
.attr('class', 'day-cell-tooltip')
|
||||
.html(tooltipHTMLForDate(d))
|
||||
.style('left', function () { return Math.floor(i / 7) * SQUARE_LENGTH + 'px'; })
|
||||
.style('top', function () {
|
||||
return formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING) + MONTH_LABEL_PADDING * 2 + 'px';
|
||||
});
|
||||
})
|
||||
.on('mouseout', function (d, i) {
|
||||
tooltip.remove();
|
||||
});
|
||||
}
|
||||
|
||||
if (chart.legendEnabled()) {
|
||||
var colorRange = [color(0)];
|
||||
for (var i = 3; i > 0; i--) {
|
||||
colorRange.push(color(max / i));
|
||||
}
|
||||
|
||||
var legendGroup = svg.append('g');
|
||||
legendGroup.selectAll('.calendar-heatmap-legend')
|
||||
.data(colorRange)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'calendar-heatmap-legend')
|
||||
.attr('width', SQUARE_LENGTH)
|
||||
.attr('height', SQUARE_LENGTH)
|
||||
.attr('x', function (d, i) { return (width - legendWidth) + (i + 1) * 13; })
|
||||
.attr('y', height + SQUARE_PADDING)
|
||||
.attr('fill', function (d) { return d; });
|
||||
|
||||
legendGroup.append('text')
|
||||
.attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-less')
|
||||
.attr('x', width - legendWidth - 13)
|
||||
.attr('y', height + SQUARE_LENGTH)
|
||||
.text(locale.Less);
|
||||
|
||||
legendGroup.append('text')
|
||||
.attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-more')
|
||||
.attr('x', (width - legendWidth + SQUARE_PADDING) + (colorRange.length + 1) * 13)
|
||||
.attr('y', height + SQUARE_LENGTH)
|
||||
.text(locale.More);
|
||||
}
|
||||
|
||||
dayRects.exit().remove();
|
||||
var monthLabels = svg.selectAll('.month')
|
||||
.data(monthRange)
|
||||
.enter().append('text')
|
||||
.attr('class', 'month-name')
|
||||
.text(function (d) {
|
||||
return locale.months[d.getMonth()];
|
||||
})
|
||||
.attr('x', function (d, i) {
|
||||
var matchIndex = 0;
|
||||
dateRange.find(function (element, index) {
|
||||
matchIndex = index;
|
||||
return moment(d).isSame(element, 'month') && moment(d).isSame(element, 'year');
|
||||
});
|
||||
|
||||
return Math.floor(matchIndex / 7) * (SQUARE_LENGTH + SQUARE_PADDING);
|
||||
})
|
||||
.attr('y', 0); // fix these to the top
|
||||
|
||||
locale.days.forEach(function (day, index) {
|
||||
index = formatWeekday(index);
|
||||
if (index % 2) {
|
||||
svg.append('text')
|
||||
.attr('class', 'day-initial')
|
||||
.attr('transform', 'translate(-8,' + (SQUARE_LENGTH + SQUARE_PADDING) * (index + 1) + ')')
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('dy', '2')
|
||||
.text(day);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pluralizedTooltipUnit (count) {
|
||||
if ('string' === typeof tooltipUnit) {
|
||||
return (tooltipUnit + (count === 1 ? '' : 's'));
|
||||
}
|
||||
for (var i in tooltipUnit) {
|
||||
var _rule = tooltipUnit[i];
|
||||
var _min = _rule.min;
|
||||
var _max = _rule.max || _rule.min;
|
||||
_max = _max === 'Infinity' ? Infinity : _max;
|
||||
if (count >= _min && count <= _max) {
|
||||
return _rule.unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tooltipHTMLForDate(d) {
|
||||
var dateStr = moment(d).format('ddd, MMM Do YYYY');
|
||||
var count = countForDate(d);
|
||||
return '<span><strong>' + (count ? count : locale.No) + ' ' + pluralizedTooltipUnit(count) + '</strong> ' + locale.on + ' ' + dateStr + '</span>';
|
||||
}
|
||||
|
||||
function countForDate(d) {
|
||||
var key= moment(d).format( 'YYYY-MM-DD' );
|
||||
return counterMap[key] || 0;
|
||||
}
|
||||
|
||||
function formatWeekday(weekDay) {
|
||||
if (weekStart === 1) {
|
||||
if (weekDay === 0) {
|
||||
return 6;
|
||||
} else {
|
||||
return weekDay - 1;
|
||||
}
|
||||
}
|
||||
return weekDay;
|
||||
}
|
||||
|
||||
var daysOfChart = chart.data().map(function (day) {
|
||||
return day.date.toDateString();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
|
||||
// polyfill for Array.find() method
|
||||
/* jshint ignore:start */
|
||||
if (!Array.prototype.find) {
|
||||
Array.prototype.find = function (predicate) {
|
||||
if (this === null) {
|
||||
throw new TypeError('Array.prototype.find called on null or undefined');
|
||||
}
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = list.length >>> 0;
|
||||
var thisArg = arguments[1];
|
||||
var value;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
value = list[i];
|
||||
if (predicate.call(thisArg, value, i, list)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
/* jshint ignore:end */
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// https://d3js.org Version 4.13.0. Copyright 2018 Mike Bostock.
|
||||
(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}function e(t){return 1===t.length&&(t=function(t){return function(e,r){return n(t(e),r)}}(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r<i;){var o=r+i>>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r<i;){var o=r+i>>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}function r(t,n){return[t,n]}function i(t){return null===t?NaN:+t}function o(t,n){var e,r,o=t.length,u=0,a=-1,c=0,s=0;if(null==n)for(;++a<o;)isNaN(e=i(t[a]))||(s+=(r=e-c)*(e-(c+=r/++u)));else for(;++a<o;)isNaN(e=i(n(t[a],a,t)))||(s+=(r=e-c)*(e-(c+=r/++u)));if(u>1)return s/(u-1)}function u(t,n){var e=o(t,n);return e?Math.sqrt(e):e}function a(t,n){var e,r,i,o=t.length,u=-1;if(null==n){for(;++u<o;)if(null!=(e=t[u])&&e>=e)for(r=i=e;++u<o;)null!=(e=t[u])&&(r>e&&(r=e),i<e&&(i=e))}else for(;++u<o;)if(null!=(e=n(t[u],u,t))&&e>=e)for(r=i=e;++u<o;)null!=(e=n(t[u],u,t))&&(r>e&&(r=e),i<e&&(i=e));return[r,i]}function c(t){return function(){return t}}function s(t){return t}function f(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r<i;)o[r]=t+r*e;return o}function l(t,n,e){var r,i,o,u,a=-1;if(n=+n,t=+t,e=+e,t===n&&e>0)return[t];if((r=n<t)&&(i=t,t=n,n=i),0===(u=h(t,n,e))||!isFinite(u))return[];if(u>0)for(t=Math.ceil(t/u),n=Math.floor(n/u),o=new Array(i=Math.ceil(n-t+1));++a<i;)o[a]=(t+a)*u;else for(t=Math.floor(t*u),n=Math.ceil(n*u),o=new Array(i=Math.ceil(t-n+1));++a<i;)o[a]=(t-a)/u;return r&&o.reverse(),o}function h(t,n,e){var r=(n-t)/Math.max(0,e),i=Math.floor(Math.log(r)/Math.LN10),o=r/Math.pow(10,i);return i>=0?(o>=Hs?10:o>=js?5:o>=Xs?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=Hs?10:o>=js?5:o>=Xs?2:1)}function p(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Hs?i*=10:o>=js?i*=5:o>=Xs&&(i*=2),n<t?-i:i}function d(t){return Math.ceil(Math.log(t.length)/Math.LN2)+1}function v(t,n,e){if(null==e&&(e=i),r=t.length){if((n=+n)<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,o=(r-1)*n,u=Math.floor(o),a=+e(t[u],u,t);return a+(+e(t[u+1],u+1,t)-a)*(o-u)}}function g(t){for(var n,e,r,i=t.length,o=-1,u=0;++o<i;)u+=t[o].length;for(e=new Array(u);--i>=0;)for(n=(r=t[i]).length;--n>=0;)e[--u]=r[n];return e}function _(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o<i;)if(null!=(e=t[o])&&e>=e)for(r=e;++o<i;)null!=(e=t[o])&&r>e&&(r=e)}else for(;++o<i;)if(null!=(e=n(t[o],o,t))&&e>=e)for(r=e;++o<i;)null!=(e=n(t[o],o,t))&&r>e&&(r=e);return r}function y(t){if(!(i=t.length))return[];for(var n=-1,e=_(t,m),r=new Array(e);++n<e;)for(var i,o=-1,u=r[n]=new Array(i);++o<i;)u[o]=t[o][n];return r}function m(t){return t.length}function x(t){return t}function b(t){return"translate("+(t+.5)+",0)"}function w(t){return"translate(0,"+(t+.5)+")"}function M(){return!this.__axis}function T(t,n){function e(e){var h=null==i?n.ticks?n.ticks.apply(n,r):n.domain():i,p=null==o?n.tickFormat?n.tickFormat.apply(n,r):x:o,d=Math.max(u,0)+c,v=n.range(),g=+v[0]+.5,_=+v[v.length-1]+.5,y=(n.bandwidth?function(t){var n=Math.max(0,t.bandwidth()-1)/2;return t.round()&&(n=Math.round(n)),function(e){return+t(e)+n}}:function(t){return function(n){return+t(n)}})(n.copy()),m=e.selection?e.selection():e,b=m.selectAll(".domain").data([null]),w=m.selectAll(".tick").data(h,n).order(),T=w.exit(),N=w.enter().append("g").attr("class","tick"),k=w.select("line"),S=w.select("text");b=b.merge(b.enter().insert("path",".tick").attr("class","domain").attr("stroke","#000")),w=w.merge(N),k=k.merge(N.append("line").attr("stroke","#000").attr(f+"2",s*u)),S=S.merge(N.append("text").attr("fill","#000").attr(f,s*d).attr("dy",t===$s?"0em":t===Zs?"0.71em":"0.32em")),e!==m&&(b=b.transition(e),w=w.transition(e),k=k.transition(e),S=S.transition(e),T=T.transition(e).attr("opacity",Qs).attr("transform",function(t){return isFinite(t=y(t))?l(t):this.getAttribute("transform")}),N.attr("opacity",Qs).attr("transform",function(t){var n=this.parentNode.__axis;return l(n&&isFinite(n=n(t))?n:y(t))})),T.remove(),b.attr("d",t===Gs||t==Ws?"M"+s*a+","+g+"H0.5V"+_+"H"+s*a:"M"+g+","+s*a+"V0.5H"+_+"V"+s*a),w.attr("opacity",1).attr("transform",function(t){return l(y(t))}),k.attr(f+"2",s*u),S.attr(f,s*d).text(p),m.filter(M).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===Ws?"start":t===Gs?"end":"middle"),m.each(function(){this.__axis=y})}var r=[],i=null,o=null,u=6,a=6,c=3,s=t===$s||t===Gs?-1:1,f=t===Gs||t===Ws?"x":"y",l=t===$s||t===Zs?b:w;return e.scale=function(t){return arguments.length?(n=t,e):n},e.ticks=function(){return r=Vs.call(arguments),e},e.tickArguments=function(t){return arguments.length?(r=null==t?[]:Vs.call(t),e):r.slice()},e.tickValues=function(t){return argumeLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function d(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function h(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n<e.length;++n)s.push(t(e[n],n));return s}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function _(e,t){for(var n in t)m(t,n)&&(e[n]=t[n]);return m(t,"toString")&&(e.toString=t.toString),m(t,"valueOf")&&(e.valueOf=t.valueOf),e}function y(e,t,n,s){return Ot(e,t,n,s,!0).utc()}function g(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function p(e){if(null==e._isValid){var t=g(e),n=i.call(t.parsedDateParts,function(e){return null!=e}),s=!isNaN(e._d.getTime())&&t.overflow<0&&!t.empty&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n);if(e._strict&&(s=s&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour),null!=Object.isFrozen&&Object.isFrozen(e))return s;e._isValid=s}return e._isValid}function v(e){var t=y(NaN);return null!=e?_(g(t),e):g(t).userInvalidated=!0,t}i=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,s=0;s<n;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1};var r=c.momentProperties=[];function w(e,t){var n,s,i;if(l(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),l(t._i)||(e._i=t._i),l(t._f)||(e._f=t._f),l(t._l)||(e._l=t._l),l(t._strict)||(e._strict=t._strict),l(t._tzm)||(e._tzm=t._tzm),l(t._isUTC)||(e._isUTC=t._isUTC),l(t._offset)||(e._offset=t._offset),l(t._pf)||(e._pf=g(t)),l(t._locale)||(e._locale=t._locale),0<r.length)for(n=0;n<r.length;n++)l(i=t[s=r[n]])||(e[s]=i);return e}var t=!1;function M(e){w(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===t&&(t=!0,c.updateOffset(this),t=!1)}function S(e){return e instanceof M||null!=e&&null!=e._isAMomentObject}function D(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function k(e){var t=+e,n=0;return 0!==t&&isFinite(t)&&(n=D(t)),n}function a(e,t,n){var s,i=Math.min(e.length,t.length),r=Math.abs(e.length-t.length),a=0;for(s=0;s<i;s++)(n&&e[s]!==t[s]||!n&&k(e[s])!==k(t[s]))&&a++;return a+r}function Y(e){!1===c.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function n(i,r){var a=!0;return _(function(){if(null!=c.deprecationHandler&&c.deprecationHandler(null,i),a){for(var e,t=[],n=0;n<arguments.length;n++){if(e="","object"==typeof arguments[n]){for(var s in e+="\n["+n+"] ",arguments[0])e+=s+": "+arguments[0][s]+", ";e=e.slice(0,-2)}else e=arguments[n];t.push(e)}Y(i+"\nArguments: "+Array.prototype.slice.call(t).join("")+"\n"+(new Error).stack),a=!1}return r.apply(this,arguments)},r)}var s,O={};function T(e,t){null!=c.deprecationHandler&&c.deprecationHandler(e,t),O[e]||(Y(t),O[e]=!0)}function x(e){return e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function b(e,t){var n,s=_({},e);for(n in t)m(t,n)&&(u(e[n])&&u(t[n])?(s[n]={},_(s[n],e[n]),_(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)m(e,n)&&!m(t,n)&&u(e[n])&&(s[n]=_({},s[n]));return s}function P(e){null!=e&&this.set(e)}c.suppressDeprecationWarnings=!1,c.deprecationHandler=null,s=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)m(e,t)&&n.push(t);return n};var W={};function H(e,t){var n=e.toLowerCase();W[n]=W[n+"s"]=W[t]=e}function R(e){return"string"==typeof e?W[e]||W[e.toLowerCase()]:void 0}function C(e){var t,n,s={};for(n in e)m(e,n)&&(t=R(n))&&(s[t]=e[n]);return s}var F={};function L(e,t){F[e]=t}function U(e,t,n){var s=""+Math.abs(e),i=t-s.length;return(0<=e?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+s}var N=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,G=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,V={},E={};function I(e,t,n,s){var i=s;"string"==typeof s&&(i=function(){return this[s]()}),e&&(E[e]=i),t&&(E[t[0]]=function(){return U(i.apply(this,arguments),t[1],t[2])}),n&&(E[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function A(e,t){return e.isValid()?(t=j(t,e.localeData()),V[t]=V[t]||function(s){var e,i,t,r=s.match(N);for(e=0,i=r.lengtLine truncated
|
||||
@@ -324,6 +324,13 @@ func mustEnableIssuesOrPulls(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustEnableUserHeatmap(ctx *context.Context) {
|
||||
if !setting.Service.EnableUserHeatmap {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all v1 APIs routes to web application.
|
||||
// FIXME: custom form error response
|
||||
func RegisterRoutes(m *macaron.Macaron) {
|
||||
@@ -348,6 +355,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||
|
||||
m.Group("/:username", func() {
|
||||
m.Get("", user.GetInfo)
|
||||
m.Get("/heatmap", mustEnableUserHeatmap, user.GetUserHeatmapData)
|
||||
|
||||
m.Get("/repos", user.ListUserRepos)
|
||||
m.Group("/tokens", func() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
@@ -34,3 +35,10 @@ type swaggerModelEditUserOption struct {
|
||||
// in:body
|
||||
Options api.EditUserOption
|
||||
}
|
||||
|
||||
// UserHeatmapData
|
||||
// swagger:response UserHeatmapData
|
||||
type swaggerResponseUserHeatmapData struct {
|
||||
// in:body
|
||||
Body []models.UserHeatmapData `json:"body"`
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
@@ -133,3 +134,41 @@ func GetAuthenticatedUser(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/User"
|
||||
ctx.JSON(200, ctx.User.APIFormat())
|
||||
}
|
||||
|
||||
// GetUserHeatmapData is the handler to get a users heatmap
|
||||
func GetUserHeatmapData(ctx *context.APIContext) {
|
||||
// swagger:operation GET /users/{username}/heatmap user userGetHeatmapData
|
||||
// ---
|
||||
// summary: Get a user's heatmap
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserHeatmapData"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// Get the user to throw an error if it does not exist
|
||||
user, err := models.GetUserByName(ctx.Params(":username"))
|
||||
if err != nil {
|
||||
if models.IsErrUserNotExist(err) {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
heatmap, err := models.GetUserHeatmapDataByUser(user)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, heatmap)
|
||||
}
|
||||
@@ -99,6 +99,8 @@ func Dashboard(ctx *context.Context) {
|
||||
ctx.Data["PageIsDashboard"] = true
|
||||
ctx.Data["PageIsNews"] = true
|
||||
ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
|
||||
ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
|
||||
ctx.Data["HeatmapUser"] = ctxUser.Name
|
||||
|
||||
var err error
|
||||
var mirrors []*models.Repository
|
||||
|
||||
@@ -87,6 +87,8 @@ func Profile(ctx *context.Context) {
|
||||
ctx.Data["PageIsUserProfile"] = true
|
||||
ctx.Data["Owner"] = ctxUser
|
||||
ctx.Data["OpenIDs"] = openIDs
|
||||
ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
|
||||
ctx.Data["HeatmapUser"] = ctxUser.Name
|
||||
showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
|
||||
|
||||
orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
|
||||
|
||||
@@ -49,6 +49,28 @@
|
||||
<script src="https://www.google.com/recaptcha/api.js" async></script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .EnableHeatmap}}
|
||||
<script src="{{AppSubUrl}}/vendor/plugins/moment/moment.min.js" charset="utf-8"></script>
|
||||
<script src="{{AppSubUrl}}/vendor/plugins/d3/d3.v4.min.js" charset="utf-8"></script>
|
||||
<script src="{{AppSubUrl}}/vendor/plugins/calendar-heatmap/calendar-heatmap.js" charset="utf-8"></script>
|
||||
<script type="text/javascript">
|
||||
$.get( '{{AppSubUrl}}/api/v1/users/{{.HeatmapUser}}/heatmap', function( chartRawData ) {
|
||||
var chartData = [];
|
||||
for (var i = 0; i < chartRawData.length; i++) {
|
||||
chartData[i] = {date: new Date(chartRawData[i].timestamp * 1000), count: chartRawData[i].contributions};
|
||||
}
|
||||
|
||||
$('#loading-heatmap').removeClass('active');
|
||||
|
||||
var heatmap = calendarHeatmap()
|
||||
.data(chartData)
|
||||
.selector('#user-heatmap')
|
||||
.colorRange(['#f4f4f4', '#459928'])
|
||||
.tooltipEnabled(true);
|
||||
heatmap();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{if .RequireTribute}}
|
||||
<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@
|
||||
{{end}}
|
||||
{{if .RequireDropzone}}
|
||||
<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.css">
|
||||
{{end}}
|
||||
{{if .EnableHeatmap}}
|
||||
<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/calendar-heatmap/calendar-heatmap.css">
|
||||
{{end}}
|
||||
<style class="list-search-style"></style>
|
||||
|
||||
|
||||
@@ -5494,6 +5494,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{username}/heatmap": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Get a user's heatmap",
|
||||
"operationId": "userGetHeatmapData",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to get",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/UserHeatmapData"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{username}/keys": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -7666,6 +7695,12 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||
},
|
||||
"TimeStamp": {
|
||||
"description": "TimeStamp defines a timestamp",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-package": "code.gitea.io/gitea/modules/util"
|
||||
},
|
||||
"TrackedTime": {
|
||||
"description": "TrackedTime worked time for an issue / pr",
|
||||
"type": "object",
|
||||
@@ -7737,6 +7772,21 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||
},
|
||||
"UserHeatmapData": {
|
||||
"description": "UserHeatmapData represents the data needed to create a heatmap",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contributions": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Contributions"
|
||||
},
|
||||
"timestamp": {
|
||||
"$ref": "#/definitions/TimeStamp"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/models"
|
||||
},
|
||||
"WatchInfo": {
|
||||
"description": "WatchInfo represents an API watch status of one repository",
|
||||
"type": "object",
|
||||
@@ -8083,6 +8133,15 @@
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
},
|
||||
"UserHeatmapData": {
|
||||
"description": "UserHeatmapData",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserHeatmapData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserList": {
|
||||
"description": "UserList",
|
||||
"schema": {
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
{{template "base/alert" .}}
|
||||
<div class="ui mobile reversed stackable grid">
|
||||
<div class="ten wide column">
|
||||
{{if .EnableHeatmap}}
|
||||
<div class="ui active centered inline indeterminate text loader" id="loading-heatmap">{{.i18n.Tr "user.heatmap.loading"}}</div>
|
||||
<div id="user-heatmap"></div>
|
||||
<div class="ui divider"></div>
|
||||
{{end}}
|
||||
{{template "user/dashboard/feeds" .}}
|
||||
</div>
|
||||
<div id="app" class="six wide column">
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
</div>
|
||||
|
||||
{{if eq .TabName "activity"}}
|
||||
{{if .EnableHeatmap}}
|
||||
<div class="ui active centered inline indeterminate text loader" id="loading-heatmap">{{.i18n.Tr "user.heatmap.loading"}}</div>
|
||||
<div id="user-heatmap"></div>
|
||||
<div class="ui divider"></div>
|
||||
{{end}}
|
||||
<div class="feeds">
|
||||
{{template "user/dashboard/feeds" .}}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user