Reworked statistics with HoloViews/Bokeh

This commit is contained in:
Kumi 2021-02-21 20:13:22 +01:00
parent 49ae5ef7d9
commit 261531f9bf
14 changed files with 922 additions and 11 deletions

View file

@ -2,6 +2,8 @@ from django import template
from io import BytesIO
import holoviews as hv
import base64
register = template.Library()
@ -11,4 +13,18 @@ def pildata(image):
data = BytesIO()
image.save(data, "JPEG")
content = base64.b64encode(data.getvalue()).decode("UTF-8")
return f"data:img/jpeg;base64,{content}"
return f"data:img/jpeg;base64,{content}"
@register.simple_tag
def hvhtml(hvobject):
renderer = hv.renderer('bokeh')
html = renderer.html(hvobject, resources="inline")
html = html.replace("http://localhost:5006/static/extensions/panel/css", "/static/frontend/vendor/panel")
return html
@register.simple_tag
def hvdata(hvobject):
html = hvhtml(hvobject)
return f"data:text/html;charset=utf-8,{html}"

View file

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def querystring(context, **kwargs):
string = context["request"].GET.urlencode()
return f"?{string}" if string else ""

View file

@ -0,0 +1,136 @@
.bk.alert {
padding: 0.75rem 1.25rem;
border: 1px solid transparent;
border-radius: 0.25rem;
/* Don't set margin because that will not render correctly! */
/* margin-bottom: 1rem; */
margin-top: 15px;
margin-bottom: 15px;
}
.bk.alert a {
color: rgb(11, 46, 19); /* #002752; */
font-weight: 700;
text-decoration: rgb(11, 46, 19);
text-decoration-color: rgb(11, 46, 19);
text-decoration-line: none;
text-decoration-style: solid;
text-decoration-thickness: auto;
}
.bk.alert a:hover {
color: rgb(11, 46, 19);
font-weight: 700;
text-decoration: underline;
}
.bk.alert-primary {
color: #004085;
background-color: #cce5ff;
border-color: #b8daff;
}
.bk.alert-primary hr {
border-top-color: #9fcdff;
}
.bk.alert-secondary {
color: #383d41;
background-color: #e2e3e5;
border-color: #d6d8db;
}
.bk.alert-secondary hr {
border-top-color: #c8cbcf;
}
.bk.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.bk.alert-success hr {
border-top-color: #b1dfbb;
}
.bk.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.bk.alert-info hr {
border-top-color: #abdde5;
}
.bk.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
.bk.alert-warning hr {
border-top-color: #ffe8a1;
}
.bk.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.bk.alert-danger hr {
border-top-color: #f1b0b7;
}
.bk.alert-light {
color: #818182;
background-color: #fefefe;
border-color: #fdfdfe;
}
.bk.alert-light hr {
border-top-color: #ececf6;
}
.bk.alert-dark {
color: #1b1e21;
background-color: #d6d8d9;
border-color: #c6c8ca;
}
.bk.alert-dark hr {
border-top-color: #b9bbbe;
}
/* adjfæl */
.bk.alert-primary a {
color: #002752;
}
.bk.alert-secondary a {
color: #202326;
}
.bk.alert-success a {
color: #0b2e13;
}
.bk.alert-info a {
color: #062c33;
}
.bk.alert-warning a {
color: #533f03;
}
.bk.alert-danger a {
color: #491217;
}
.bk.alert-light a {
color: #686868;
}
.bk.alert-dark a {
color: #040505;
}

View file

@ -0,0 +1,43 @@
.bk.card {
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
}
.bk.accordion {
border: 1px solid rgba(0,0,0,.125);
}
.bk.card-header {
align-items: center;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 0.25rem;
display: flex;
justify-content: space-between;
padding: 0 1.25rem 0 0;
width: 100%;
}
.bk.accordion-header {
align-items: center;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 0;
display: flex;
justify-content: space-between;
padding: 0 1.25rem 0 0;
width: 100%;
}
p.bk.card-button {
background-color: transparent;
font-size: 1.25rem;
font-weight: 700;
margin: 0;
margin-left: -15px;
}
.bk.card-header-row {
position: relative !important;
}
.bk.card-title {
align-items: center;
display: flex !important;
font-size: 1.4em;
font-weight: bold;
padding: 0.25em;
position: relative !important;
}

View file

@ -0,0 +1,41 @@
table.panel-df {
margin-left: auto;
margin-right: auto;
border: none;
border-collapse: collapse;
border-spacing: 0;
color: black;
font-size: 12px;
table-layout: fixed;
width: 100%;
}
.panel-df tr, .panel-df th, .panel-df td {
text-align: right;
vertical-align: middle;
padding: 0.5em 0.5em !important;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
.panel-df tbody {
display: table-row-group;
vertical-align: middle;
border-color: inherit;
}
.panel-df tbody tr:nth-child(odd) {
background: #f5f5f5;
}
.panel-df thead {
border-bottom: 1px solid black;
vertical-align: bottom;
}
.panel-df tr:hover {
background: lightblue !important;
cursor: pointer;
}

View file

@ -0,0 +1,194 @@
.json-formatter-row {
font-family: monospace;
}
.json-formatter-row,
.json-formatter-row a,
.json-formatter-row a:hover {
color: black;
text-decoration: none;
}
.json-formatter-row .json-formatter-row {
margin-left: 1rem;
}
.json-formatter-row .json-formatter-children.json-formatter-empty {
opacity: 0.5;
margin-left: 1rem;
}
.json-formatter-row .json-formatter-children.json-formatter-empty:after {
display: none;
}
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
content: "No properties";
}
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
content: "[]";
}
.json-formatter-row .json-formatter-string,
.json-formatter-row .json-formatter-stringifiable {
color: green;
white-space: pre;
word-wrap: break-word;
}
.json-formatter-row .json-formatter-number {
color: blue;
}
.json-formatter-row .json-formatter-boolean {
color: red;
}
.json-formatter-row .json-formatter-null {
color: #855A00;
}
.json-formatter-row .json-formatter-undefined {
color: #ca0b69;
}
.json-formatter-row .json-formatter-function {
color: #FF20ED;
}
.json-formatter-row .json-formatter-date {
background-color: rgba(0, 0, 0, 0.05);
}
.json-formatter-row .json-formatter-url {
text-decoration: underline;
color: blue;
cursor: pointer;
}
.json-formatter-row .json-formatter-bracket {
color: blue;
}
.json-formatter-row .json-formatter-key {
color: #00008B;
padding-right: 0.2rem;
}
.json-formatter-row .json-formatter-toggler-link {
cursor: pointer;
}
.json-formatter-row .json-formatter-toggler {
line-height: 1.2rem;
font-size: 0.7rem;
vertical-align: middle;
opacity: 0.6;
cursor: pointer;
padding-right: 0.2rem;
}
.json-formatter-row .json-formatter-toggler:after {
display: inline-block;
transition: transform 100ms ease-in;
content: "\25BA";
}
.json-formatter-row > a > .json-formatter-preview-text {
opacity: 0;
transition: opacity 0.15s ease-in;
font-style: italic;
}
.json-formatter-row:hover > a > .json-formatter-preview-text {
opacity: 0.6;
}
.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
transform: rotate(90deg);
}
.json-formatter-row.json-formatter-open > .json-formatter-children:after {
display: inline-block;
}
.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
display: none;
}
.json-formatter-row.json-formatter-open.json-formatter-empty:after {
display: block;
}
.json-formatter-dark.json-formatter-row {
font-family: monospace;
}
.json-formatter-dark.json-formatter-row,
.json-formatter-dark.json-formatter-row a,
.json-formatter-dark.json-formatter-row a:hover {
color: white;
text-decoration: none;
}
.json-formatter-dark.json-formatter-row .json-formatter-row {
margin-left: 1rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty {
opacity: 0.5;
margin-left: 1rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty:after {
display: none;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
content: "No properties";
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
content: "[]";
}
.json-formatter-dark.json-formatter-row .json-formatter-string,
.json-formatter-dark.json-formatter-row .json-formatter-stringifiable {
color: #31F031;
white-space: pre;
word-wrap: break-word;
}
.json-formatter-dark.json-formatter-row .json-formatter-number {
color: #66C2FF;
}
.json-formatter-dark.json-formatter-row .json-formatter-boolean {
color: #EC4242;
}
.json-formatter-dark.json-formatter-row .json-formatter-null {
color: #EEC97D;
}
.json-formatter-dark.json-formatter-row .json-formatter-undefined {
color: #ef8fbe;
}
.json-formatter-dark.json-formatter-row .json-formatter-function {
color: #FD48CB;
}
.json-formatter-dark.json-formatter-row .json-formatter-date {
background-color: rgba(255, 255, 255, 0.05);
}
.json-formatter-dark.json-formatter-row .json-formatter-url {
text-decoration: underline;
color: #027BFF;
cursor: pointer;
}
.json-formatter-dark.json-formatter-row .json-formatter-bracket {
color: #9494FF;
}
.json-formatter-dark.json-formatter-row .json-formatter-key {
color: #23A0DB;
padding-right: 0.2rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler-link {
cursor: pointer;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler {
line-height: 1.2rem;
font-size: 0.7rem;
vertical-align: middle;
opacity: 0.6;
cursor: pointer;
padding-right: 0.2rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler:after {
display: inline-block;
transition: transform 100ms ease-in;
content: "\25BA";
}
.json-formatter-dark.json-formatter-row > a > .json-formatter-preview-text {
opacity: 0;
transition: opacity 0.15s ease-in;
font-style: italic;
}
.json-formatter-dark.json-formatter-row:hover > a > .json-formatter-preview-text {
opacity: 0.6;
}
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
transform: rotate(90deg);
}
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-children:after {
display: inline-block;
}
.json-formatter-dark.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
display: none;
}
.json-formatter-dark.json-formatter-row.json-formatter-open.json-formatter-empty:after {
display: block;
}

View file

@ -0,0 +1,81 @@
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #f8f8f8; }
.codehilite .c { color: #408080; font-style: italic } /* Comment */
.codehilite .err { border: 1px solid #FF0000 } /* Error */
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.codehilite .o { color: #666666 } /* Operator */
.codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
.codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .gr { color: #FF0000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #00A000 } /* Generic.Inserted */
.codehilite .go { color: #888888 } /* Generic.Output */
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #0044DD } /* Generic.Traceback */
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #B00040 } /* Keyword.Type */
.codehilite .m { color: #666666 } /* Literal.Number */
.codehilite .s { color: #BA2121 } /* Literal.String */
.codehilite .na { color: #7D9029 } /* Name.Attribute */
.codehilite .nb { color: #008000 } /* Name.Builtin */
.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.codehilite .no { color: #880000 } /* Name.Constant */
.codehilite .nd { color: #AA22FF } /* Name.Decorator */
.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.codehilite .nf { color: #0000FF } /* Name.Function */
.codehilite .nl { color: #A0A000 } /* Name.Label */
.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #19177C } /* Name.Variable */
.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #666666 } /* Literal.Number.Bin */
.codehilite .mf { color: #666666 } /* Literal.Number.Float */
.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
.codehilite .sx { color: #008000 } /* Literal.String.Other */
.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #0000FF } /* Name.Function.Magic */
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
.markdown h1 { margin-block-start: 0.34em }
.markdown h2 { margin-block-start: 0.42em }
.markdown h3 { margin-block-start: 0.5em }
.markdown h4 { margin-block-start: 0.67em }
.markdown h5 { margin-block-start: 0.84em }
.markdown h6 { margin-block-start: 1.17em }
.markdown ul { padding-inline-start: 2em }
.markdown ol { padding-inline-start: 2em }
.markdown strong { font-weight: 600 }
.markdown a { color: -webkit-link }
.markdown a { color: -moz-hyperlinkText }

View file

@ -0,0 +1,288 @@
.bk.panel-widget-box {
min-height: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
overflow-x: hidden;
overflow-y: hidden;
}
.scrollable {
overflow: scroll;
}
progress {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
border: none;
height: 20px;
background-color: whiteSmoke;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0,0,0,.5) inset;
color: royalblue;
position: relative;
margin: 0 0 1.5em;
}
progress[value]::-webkit-progress-bar {
background-color: whiteSmoke;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0,0,0,.5) inset;
}
progress[value]::-webkit-progress-value {
position: relative;
background-size: 35px 20px, 100% 100%, 100% 100%;
border-radius:3px;
}
progress.active:not([value])::before {
background-position: 10%;
animation-name: stripes;
animation-duration: 3s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
progress[value]::-moz-progress-bar {
background-size: 35px 20px, 100% 100%, 100% 100%;
border-radius:3px;
}
progress:not([value])::-moz-progress-bar {
border-radius:3px;
background:
linear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;
}
progress.active:not([value])::-moz-progress-bar {
background-position: 10%;
animation-name: stripes;
animation-duration: 3s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
progress.active:not([value])::-webkit-progress-bar {
background-position: 10%;
animation-name: stripes;
animation-duration: 3s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
progress.primary[value]::-webkit-progress-value { background-color: #007bff; }
progress.primary:not([value])::before { background-color: #007bff; }
progress.primary:not([value])::-webkit-progress-bar { background-color: #007bff; }
progress.primary::-moz-progress-bar { background-color: #007bff; }
progress.secondary[value]::-webkit-progress-value { background-color: #6c757d; }
progress.secondary:not([value])::before { background-color: #6c757d; }
progress.secondary:not([value])::-webkit-progress-bar { background-color: #6c757d; }
progress.secondary::-moz-progress-bar { background-color: #6c757d; }
progress.success[value]::-webkit-progress-value { background-color: #28a745; }
progress.success:not([value])::before { background-color: #28a745; }
progress.success:not([value])::-webkit-progress-bar { background-color: #28a745; }
progress.success::-moz-progress-bar { background-color: #28a745; }
progress.danger[value]::-webkit-progress-value { background-color: #dc3545; }
progress.danger:not([value])::before { background-color: #dc3545; }
progress.danger:not([value])::-webkit-progress-bar { background-color: #dc3545; }
progress.danger::-moz-progress-bar { background-color: #dc3545; }
progress.warning[value]::-webkit-progress-value { background-color: #ffc107; }
progress.warning:not([value])::before { background-color: #ffc107; }
progress.warning:not([value])::-webkit-progress-bar { background-color: #ffc107; }
progress.warning::-moz-progress-bar { background-color: #ffc107; }
progress.info[value]::-webkit-progress-value { background-color: #17a2b8; }
progress.info:not([value])::before { background-color: #17a2b8; }
progress.info:not([value])::-webkit-progress-bar { background-color: #17a2b8; }
progress.info::-moz-progress-bar { background-color: #17a2b8; }
progress.light[value]::-webkit-progress-value { background-color: #f8f9fa; }
progress.light:not([value])::before { background-color: #f8f9fa; }
progress.light:not([value])::-webkit-progress-bar { background-color: #f8f9fa; }
progress.light::-moz-progress-bar { background-color: #f8f9fa; }
progress.dark[value]::-webkit-progress-value { background-color: #343a40; }
progress.dark:not([value])::-webkit-progress-bar { background-color: #343a40; }
progress.dark:not([value])::before { background-color: #343a40; }
progress.dark::-moz-progress-bar { background-color: #343a40; }
progress:not([value])::-webkit-progress-bar {
border-radius: 3px;
background:
linear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;
}
progress:not([value])::before {
content:" ";
position:absolute;
height: 20px;
top:0;
left:0;
right:0;
bottom:0;
border-radius: 3px;
background:
linear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, 0.2) 33%, rgba(0, 0, 0, 0.2) 66%, transparent 66%) left/2.5em 1.5em;
}
@keyframes stripes {
from {background-position: 0%}
to {background-position: 100%}
}
.bk.loader::after {
content: "";
border-radius: 50%;
-webkit-mask-image: radial-gradient(transparent 50%, rgba(0, 0, 0, 1) 54%);
width: 100%;
height: 100%;
left: 0;
top: 0;
position: absolute;
}
.bk-root .bk.loader.dark::after {
background: #0f0f0f;
}
.bk-root .bk.loader.light::after {
background: #f0f0f0;
}
.bk-root .bk.loader.spin::after {
animation: spin 2s linear infinite;
}
.bk-root div.bk.loader.spin.primary-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #007bff 50%);
}
.bk-root div.bk.loader.spin.secondary-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #6c757d 50%);
}
.bk-root div.bk.loader.spin.success-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #28a745 50%);
}
.bk-root div.bk.loader.spin.danger-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #dc3545 50%);
}
.bk-root div.bk.loader.spin.warning-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #ffc107 50%);
}
.bk-root div.bk.loader.spin.info-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #17a2b8 50%);
}
.bk-root div.bk.loader.spin.light-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #f8f9fa 50%);
}
.bk-root div.bk.loader.dark-light::after {
background: linear-gradient(135deg, #f0f0f0 50%, transparent 50%), linear-gradient(45deg, #f0f0f0 50%, #343a40 50%);
}
.bk-root div.bk.loader.spin.primary-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #007bff 50%);
}
.bk-root div.bk.loader.spin.secondary-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #6c757d 50%);
}
.bk-root div.bk.loader.spin.success-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #28a745 50%);
}
.bk-root div.bk.loader.spin.danger-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #dc3545 50%)
}
.bk-root div.bk.loader.spin.warning-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #ffc107 50%);
}
.bk-root div.bk.loader.spin.info-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #17a2b8 50%);
}
.bk-root div.bk.loader.spin.light-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #f8f9fa 50%);
}
.bk-root div.bk.loader.spin.dark-dark::after {
background: linear-gradient(135deg, #0f0f0f 50%, transparent 50%), linear-gradient(45deg, #0f0f0f 50%, #343a40 50%);
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.dot div {
height: 100%;
width: 100%;
border: 1px solid #000 !important;
background-color: #fff;
border-radius: 50%;
display: inline-block;
}
.dot-filled div {
height: 100%;
width: 100%;
border: 1px solid #000 !important;
border-radius: 50%;
display: inline-block;
}
.dot-filled.primary div {
background-color: #007bff;
}
.dot-filled.secondary div {
background-color: #6c757d;
}
.dot-filled.success div {
background-color: #28a745;
}
.dot-filled.danger div {
background-color: #dc3545;
}
.dot-filled.warning div {
background-color: #ffc107;
}
.dot-filled.info div {
background-color: #17a2b8;
}
.dot-filled.dark div {
background-color: #343a40;
}
.dot-filled.light div {
background-color: #f8f9fa;
}

View file

@ -0,0 +1,6 @@
var iframe = document.getElementById("plot");
iframe.onload = function(){
iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px';
iframe.style.width = iframe.contentWindow.document.body.scrollWidth + 'px';
}

53
mood/statistics.py Normal file
View file

@ -0,0 +1,53 @@
import holoviews as hv
import pandas as pd
from django.utils import timezone
from bokeh.models import HoverTool
from dateutil.relativedelta import relativedelta
from .models import Status
def moodstats(mindate=None, maxdate=None, days=7):
hv.extension('bokeh')
maxdate = maxdate or timezone.now()
mindate = mindate or (maxdate - relativedelta(days=days))
tooltips = [
('Date', '@date{%F %H:%M}'),
('Value', '@value')
]
formatters = {
'@date': 'datetime'
}
hover = HoverTool(tooltips=tooltips, formatters=formatters)
pointdict = {"date": [], "value": [], "color": []}
for status in Status.objects.filter(timestamp__gte=mindate, timestamp__lte=maxdate):
if status.mood:
pointdict["date"].append(status.timestamp)
pointdict["value"].append(status.mood.value)
pointdict["color"].append(status.mood.color)
pointframe = pd.DataFrame.from_dict(pointdict)
points = hv.Points(pointframe)
points.opts(
tools=[hover], color='color', cmap='Category20',
line_color='black', size=25,
width=600, height=400, show_grid=True,
title='Your Mood Entries')
pointtuples = [(pointdict["date"][i], pointdict["value"][i]) for i in range(len(pointdict["date"]))]
line = hv.Curve(pointtuples)
output = points * line
output.opts(tools=["xwheel_zoom"])
return output

View file

@ -1,4 +1,6 @@
{% extends "frontend/base.html" %}
{% load images %}
{% load request %}
{% block "content" %}
<div class="row">
@ -13,7 +15,7 @@
</div>
<!-- Card Body -->
<div class="card-body">
<div id="my_dataviz"></div>
<iframe id="plot" src="plot/{% querystring %}" width="800px" height="450px"></iframe>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
from .views import StatusListView, StatusViewView, StatusDeleteView, StatusEditView, StatusCreateView, ActivityListView, ActivityEditView, ActivityCreateView, ActivityDeleteView, MoodListView, MoodEditView, NotificationCreateView, NotificationDeleteView, NotificationEditView, NotificationListView, MoodStatisticsView, MoodCSVView
from .views import StatusListView, StatusViewView, StatusDeleteView, StatusEditView, StatusCreateView, ActivityListView, ActivityEditView, ActivityCreateView, ActivityDeleteView, MoodListView, MoodEditView, NotificationCreateView, NotificationDeleteView, NotificationEditView, NotificationListView, MoodStatisticsView, MoodCSVView, MoodPlotView
from django.urls import path, include
@ -22,4 +22,5 @@ urlpatterns = [
path('notification/new/', NotificationCreateView.as_view(), name="notification_create"),
path('statistics/', MoodStatisticsView.as_view(), name="statistics"),
path('statistics/csv/', MoodCSVView.as_view(), name="statistics_csv"),
path('statistics/plot/', MoodPlotView.as_view(), name="statistics_plot"),
]

View file

@ -4,15 +4,21 @@ from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect, HttpResponse
from django.utils import timezone
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.utils.decorators import method_decorator
from .models import Status, Activity, Mood, StatusMedia, StatusActivity
from .forms import StatusForm
from .statistics import moodstats
from common.helpers import get_upload_path
from common.templatetags.images import hvhtml
from msgio.models import NotificationDailySchedule, Notification
from dateutil import relativedelta
from datetime import datetime
class StatusListView(LoginRequiredMixin, ListView):
template_name = "mood/status_list.html"
model = Status
@ -326,8 +332,6 @@ class MoodStatisticsView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Statistics"
context["scripts"] = ["frontend/vendor/d3/d3.min.js", "mood/statistics.js"]
context["styles"] = ["mood/statistics.css"]
return context
class MoodCSVView(LoginRequiredMixin, View):
@ -335,14 +339,49 @@ class MoodCSVView(LoginRequiredMixin, View):
res = HttpResponse(content_type="text/csv")
res["content-disposition"] = 'filename="mood.csv"'
maxage = timezone.now()
minage = maxage - relativedelta.relativedelta(weeks=1)
startdate = request.GET.get("start")
enddate = request.GET.get("end")
if enddate:
maxdate = datetime.strptime(enddate, "%Y-%m-%d")
else:
maxdate = timezone.now()
if startdate:
mindate = datetime.strptime(startdate, "%Y-%m-%d")
else:
mindate = maxdate - relativedelta.relativedelta(weeks=1)
output = "date,value"
for status in Status.objects.filter(user=request.user, timestamp__gte=minage, timestamp__lte=maxage):
date = status.timestamp.strftime("%Y-%m-%d %H:%M")
output += f"\n{date},{status.mood.value}"
for status in Status.objects.filter(user=request.user, timestamp__gte=mindate, timestamp__lte=maxdate):
if status.mood:
date = status.timestamp.strftime("%Y-%m-%d %H:%M")
output += f"\n{date},{status.mood.value}"
res.write(output)
return res
class MoodPlotView(LoginRequiredMixin, View):
@method_decorator(xframe_options_sameorigin)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
res = HttpResponse(content_type="text/html")
startdate = request.GET.get("start")
enddate = request.GET.get("end")
if enddate:
maxdate = datetime.strptime(enddate, "%Y-%m-%d")
else:
maxdate = timezone.now()
if startdate:
mindate = datetime.strptime(startdate, "%Y-%m-%d")
else:
mindate = maxdate - relativedelta.relativedelta(weeks=1)
res.write(hvhtml(moodstats(mindate, maxdate)))
return res

View file

@ -14,4 +14,7 @@ boto3
argon2_cffi
python-telegram-bot
python-dateutil
matrix-nio
matrix-nio
holoviews
bokeh==2.3.0dev13
panel==0.11.0a16