diff --git a/common/templatetags/images.py b/common/templatetags/images.py index 94bcd68..51d57a1 100644 --- a/common/templatetags/images.py +++ b/common/templatetags/images.py @@ -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}" \ No newline at end of file + 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}" \ No newline at end of file diff --git a/common/templatetags/request.py b/common/templatetags/request.py new file mode 100644 index 0000000..883a14e --- /dev/null +++ b/common/templatetags/request.py @@ -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 "" \ No newline at end of file diff --git a/frontend/static/frontend/vendor/panel/alerts.css b/frontend/static/frontend/vendor/panel/alerts.css new file mode 100644 index 0000000..db1cfbb --- /dev/null +++ b/frontend/static/frontend/vendor/panel/alerts.css @@ -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; +} \ No newline at end of file diff --git a/frontend/static/frontend/vendor/panel/card.css b/frontend/static/frontend/vendor/panel/card.css new file mode 100644 index 0000000..8ba4106 --- /dev/null +++ b/frontend/static/frontend/vendor/panel/card.css @@ -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; +} diff --git a/frontend/static/frontend/vendor/panel/dataframe.css b/frontend/static/frontend/vendor/panel/dataframe.css new file mode 100644 index 0000000..0b466cc --- /dev/null +++ b/frontend/static/frontend/vendor/panel/dataframe.css @@ -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; +} diff --git a/frontend/static/frontend/vendor/panel/json.css b/frontend/static/frontend/vendor/panel/json.css new file mode 100644 index 0000000..ea0d5d2 --- /dev/null +++ b/frontend/static/frontend/vendor/panel/json.css @@ -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; +} diff --git a/frontend/static/frontend/vendor/panel/markdown.css b/frontend/static/frontend/vendor/panel/markdown.css new file mode 100644 index 0000000..b74a924 --- /dev/null +++ b/frontend/static/frontend/vendor/panel/markdown.css @@ -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 } diff --git a/frontend/static/frontend/vendor/panel/widgets.css b/frontend/static/frontend/vendor/panel/widgets.css new file mode 100644 index 0000000..9a107dc --- /dev/null +++ b/frontend/static/frontend/vendor/panel/widgets.css @@ -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; +} \ No newline at end of file diff --git a/mood/static/mood/statistics_iframe.js b/mood/static/mood/statistics_iframe.js new file mode 100644 index 0000000..b1297b6 --- /dev/null +++ b/mood/static/mood/statistics_iframe.js @@ -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'; +} \ No newline at end of file diff --git a/mood/statistics.py b/mood/statistics.py new file mode 100644 index 0000000..6125e94 --- /dev/null +++ b/mood/statistics.py @@ -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 \ No newline at end of file diff --git a/mood/templates/mood/statistics.html b/mood/templates/mood/statistics.html index f0abc62..0990120 100644 --- a/mood/templates/mood/statistics.html +++ b/mood/templates/mood/statistics.html @@ -1,4 +1,6 @@ {% extends "frontend/base.html" %} +{% load images %} +{% load request %} {% block "content" %}
@@ -13,7 +15,7 @@
-
+
diff --git a/mood/urls.py b/mood/urls.py index 51343be..7f84039 100644 --- a/mood/urls.py +++ b/mood/urls.py @@ -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"), ] \ No newline at end of file diff --git a/mood/views.py b/mood/views.py index 4886c41..5ad8502 100644 --- a/mood/views.py +++ b/mood/views.py @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5ae6319..db4e101 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,7 @@ boto3 argon2_cffi python-telegram-bot python-dateutil -matrix-nio \ No newline at end of file +matrix-nio +holoviews +bokeh==2.3.0dev13 +panel==0.11.0a16 \ No newline at end of file