I've tried different solutions to animate a sidenav bar rise from bottom right corner. This is the last solution, maybe the best one ( because i'm animating the transform scale() ) in the previous ( width / height ) but the animation still sluggish on the cut corner... with the cut line acting with strange squared forms during the animation ... so i'm looking to solve!
Here the scale() example on glitch
https://glitch.com/edit/#!/heady-grey-lantern
Even on w3schools site the simple animation of Side Navigation is little shimmy and flicker
https://www.w3schools.com/howto/howto_js_sidenav.asp
Here the core of animation
// css code
.sidenav {
width: 200vw;
height: 200vh;
width: 200dvw;
height: 200dvh;
position: fixed;
bottom: 0;
/* left: 0; */
right: 0;
/* cut corner */
--cutting: max(100vw, 100vh);
--cutting: max(150dvw, 150dvh);
clip-path: polygon(0 var(--cutting), var(--cutting) 0,100% 0,100% 100%,0 100%);
/* z-index: 1; */
background-color: #111;
/* overflow-x: hidden; */
will-change: transform;
transform: scale(0);
transform-origin: bottom right;
}
// Js code
const sidenav = document.getElementById("mySidenav")
const duration = 500
const openSidenav = [
{ transform: 'scaleX(0) scaleY(0)' },
{ transform: 'scaleX(1) scaleY(1)' }
]
const closeSidenav = [
{ transform: 'scaleX(1) scaleY(1)' },
{ transform: 'scaleX(0) scaleY(0)' }
]
const openNav = () => {
console.log('#NAV >> Open')
// document.getElementById("mySidenav").style.width = '175vw'
// document.getElementById("mySidenav").style.height = '175vh'
sidenav.animate(
openSidenav, {
duration,
iterations: 1,
easing: 'ease-in',
fill: 'forwards'
}
)
}
const closeNav = () => {
console.log('#NAV >> Close')
// document.getElementById("mySidenav").style.width = '0'
// document.getElementById("mySidenav").style.height = '0'
sidenav.animate(
closeSidenav, {
duration,
iterations: 1,
easing: 'ease-in',
fill: 'forwards'
}
)
}
Last, better one!
/* css */
#sidenav {
width: 100vw;
height: 100vh;
width: 100dvw;
height: 100dvh;
position: fixed;
/* cut corner */
--cutting: max(100vw, 100vh);
clip-path: polygon(0 var(--cutting), var(--cutting) 0,100% 0,100% 100%,0 100%);
/* set transition as in WebAnimation API */
transition: all 0.5s;
will-change: transform;
transform: scale(0);
transform-origin: bottom right;
z-index: 11;
background-color: var(--surface1);
display: grid;
justify-items: center;
align-items: center;
/* justify-items: end;
align-items: end; */
}
#sidenav[active] {
--cutting: 0;
transition: all 0.5s;
}
/* --------------- Mobile Nav -------------- */
nav {
/* TODO fallback inline-size: 75vw;
block-size: 75vh; */
inline-size: 75dvw;
block-size: 75dvh;
display: block;
flex-direction: column;
background-color: crimson;
}
// javascript animations
const openSidenav = [
{ transform: 'scale(0)' },
{ transform: 'scale(0.25)' },
{ transform: 'scale(0.5)' },
{ transform: 'scale(0.75)' },
{ transform: 'scale(1)' }
]
const closeSidenav = [
{ transform: 'scale(1)' },
{ transform: 'scale(0.75)' },
{ transform: 'scale(0.5)' },
{ transform: 'scale(0.25)' },
{ transform: 'scale(0)' }
]
const slideIn = [
{ transform: 'translateX(100%)' },
{ transform: 'translateX(0)', opacity: 1 }
]
const slideOut = [
{ transform: 'translateX(0)' },
{ transform: 'translateX(100%)', opacity: 0 }
]
// duration animation
const openSidenavDuration = 500
const closeSidenavDuration = 300
// duration animation per a tag
const slideInDuration = 200
const slideOutDuration = 100
// javascript
openNav (e) {
const sidenav =
this.renderRoot.getElementById('sidenav')
this.sidenavIsOpen = true
const openAnimation = sidenav.animate(
openSidenav, {
duration: openSidenavDuration,
iterations: 1,
easing: 'ease-in',
fill: 'forwards'
}
)
openAnimation.addEventListener('finish', (e) => {
// console.log('#EVENT >> ', e.type)
sidenav.setAttribute('active', '')
// sidenav.style.setProperty('--cutting', 0)
})
}
closeNav (e) {
const sidenav =
this.renderRoot.getElementById('sidenav')
this.sidenavIsOpen = false
sidenav.animate(
closeSidenav, {
duration: closeSidenavDuration,
iterations: 1,
easing: 'ease-in',
fill: 'forwards',
delay: slideOutDuration * 3
}
)
sidenav.removeAttribute('active')
}
My custom element is called "mb-bubbles". It represent a list of things. Each of these things must have at least the attributes "id" (Number) and "text" (String). If the component have the attribute "editable", you should be able to remove items ("blubbles") from it.
Its source code is:
import { LitElement, html, css } from '../vendor/lit-2.4.0/lit-all.min.js';
export class MbBlubbles extends LitElement {
static styles = [
css`
:host {
display: block;
overflow: hidden;
}
.bubble {
font-size: 14px;
line-height: 14px;
padding: 5px 10px;
border-radius: 18px;
background: #fff;
display: block;
float: left;
margin: 4px 2px;
cursor: pointer;
}
.bubble:hover {
background: #def;
}
.bubble>.remove {
display: inline-block;
line-height: 20px;
width: 20px;
border-radius: 10px;
text-align: center;
font-weight: bold;
}
.bubble>.remove:hover {
background: #fff;
}
`
];
static get properties() {
return {
items: { type: Array }, // An array of objects with 2 attributes: "id" (Number) and "text" (String).
editable: { type: Boolean }
};
}
selectItem(item) {
this.dispatchEvent(new CustomEvent('select-item', { detail: { item } }));
}
removeItem(item) {
this.items = this.items.filter(it => it.id != item.id);
}
render() {
return this.items.map(item => html`
<div class="bubble" #click="${() => this.selectItem(item)}">
${item.text}
${this.editable ? html`<span class="remove" #click="${() => this.removeItem(item)}">×</span>` : ''}
</div>
`);
}
}
customElements.define('mb-blubbles', MbBlubbles);
This component is used from outside in this way:
<mb-blubbles .items="${this.items}" editable></mb-blubbles>
${JSON.stringify(this.items)}
As you can see the property this.items seems unmodified.
How to modify the property from inside my custom component and have its changes reflected from the outside?
Is there any directive for that?
The reactive property only works within the component class, that is, updating this.items from within <mb-blubbles> will cause the contents of it to re-render, but it won't cause the parent to re-render, because the reactivity is based on the accessor of the component class itself.
If you need this to be updated in the parent, it would be better for the removeItems() method to dispatch an event, just like you're doing with the selectItem, and have the parent listen for that and update its own this.items.
class MbBlubbles extends LitElement {
...
removeItem(item) {
this.dispatchEvent(new CustomEvent('remove-item', { detail: { item } }));
}
...
}
class ParentElement extends LitElement {
static properties = { items: { type: Array } };
removeItem(event) {
const { item } = event.detail;
this.items = this.items.filter(it => it.id != item.id);
}
render() {
return html`
<mb-blubbles #remove-item=${this.removeItem} .items=${this.items} editable></mb-blubbles>
${JSON.stringify(this.items)}
`;
}
}
Here's a working playground: https://lit.dev/playground/#gist=f61456f62076b849c0af02b2b1c7aff6
I want to make a separate tooltip for every stacked bar, Ex. My demo "2022-01-17" has TWO stacked bars with FOUR values but I need a total of Stack 1 group and Stack 2 group
I've reviewed most of the options in chartjs https://www.chartjs.org/docs/3.5.1/samples/bar/stacked-groups.html
var barChartData = {
labels: ["2022-01-17","2022-01-18","2022-01-19","2022-01-20","2022-01-21","2022-01-22","2022-01-23","2022-01-24","2022-01-25","2022-01-26","2022-01-27","2022-01-28","2022-01-29","2022-01-30"],
datasets: [{"label":"Product 2","data":["292.53","328.5","273.83","305.44","260.33","251.87","118.15","253.95","86.64","87.78","116.68","295.49","61.32","83.78"],"backgroundColor":"#66bb6a","borderColor":"#66bb6a","pointBackgroundColor":"#66bb6a","stack":"Stack 0"},{"label":"Product ","data":["1522.27","1844.83","1581.01","2767.68","2821.36","2940.31","2876.1","2037.79","1593.01","1900.86","1607.21","2188.92","2428.74","2508.81"],"backgroundColor":"#1b5e20","borderColor":"#1b5e20","pointBackgroundColor":"#1b5e20","stack":"Stack 0"},{"label":"Product 2","data":["200","4.14","28.51","13.68","0","0","19.93","0","0","0","10.47","23.05","9.42","10.58"],"backgroundColor":"#ffcdd2","borderColor":"#ffcdd2","pointBackgroundColor":"#ffcdd2","stack":"Stack 1"},{"label":"Product ","data":["680.2","536.51","524.41","479.69","453.19","521.87","530.57","485.13","440.25","591.29","722.73","711.58","686.63","510.72"],"backgroundColor":"#ef9a9a","borderColor":"#ef9a9a","pointBackgroundColor":"#ef9a9a","stack":"Stack 1"}]
};
const footer = (tooltipItems) => {
let sum = 0;
tooltipItems.forEach(function(tooltipItem) {
sum += tooltipItem.parsed.y;
});
return 'Sum: ' + sum;
};
var ctx = document.getElementById("canvas").getContext("2d");
var myBar = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
tooltip: {
callbacks: {
footer: (tooltipItem) => {
let sum = 0;
tooltipItem.forEach(function(tooltipItem) {
sum += tooltipItem.parsed.y;
});
return 'Sum: ' + sum;
}
}
}
}
}
});
<script src="https://cdn.jsdelivr.net/npm/chart.js#3.7.0/dist/chart.min.js"></script>
<canvas id="canvas" height="100"></canvas>
to get the total of each stack, you can use the dataPoints found in the tooltip context
and use the dataset labels to group by each stack
// group stacks
const groups = {};
tooltip.dataPoints.forEach(function (point) {
if (groups.hasOwnProperty(barChartData.datasets[point.datasetIndex].label)) {
groups[barChartData.datasets[point.datasetIndex].label] += parseFloat(barChartData.datasets[point.datasetIndex].data[point.dataIndex]);
} else {
groups[barChartData.datasets[point.datasetIndex].label] = parseFloat(barChartData.datasets[point.datasetIndex].data[point.dataIndex]);
}
});
e.g. --> {"Product 2":492.53,"Product ":2202.4700000000003}
then use the external option to create a custom tooltip
see following working snippet...
$(document).ready(function() {
var barChartData = {
labels: ["2022-01-17","2022-01-18","2022-01-19","2022-01-20","2022-01-21","2022-01-22","2022-01-23","2022-01-24","2022-01-25","2022-01-26","2022-01-27","2022-01-28","2022-01-29","2022-01-30"],
datasets: [{"label":"Product 2","data":["292.53","328.5","273.83","305.44","260.33","251.87","118.15","253.95","86.64","87.78","116.68","295.49","61.32","83.78"],"backgroundColor":"#66bb6a","borderColor":"#66bb6a","pointBackgroundColor":"#66bb6a","stack":"Stack 0"},{"label":"Product ","data":["1522.27","1844.83","1581.01","2767.68","2821.36","2940.31","2876.1","2037.79","1593.01","1900.86","1607.21","2188.92","2428.74","2508.81"],"backgroundColor":"#1b5e20","borderColor":"#1b5e20","pointBackgroundColor":"#1b5e20","stack":"Stack 0"},{"label":"Product 2","data":["200","4.14","28.51","13.68","0","0","19.93","0","0","0","10.47","23.05","9.42","10.58"],"backgroundColor":"#ffcdd2","borderColor":"#ffcdd2","pointBackgroundColor":"#ffcdd2","stack":"Stack 1"},{"label":"Product ","data":["680.2","536.51","524.41","479.69","453.19","521.87","530.57","485.13","440.25","591.29","722.73","711.58","686.63","510.72"],"backgroundColor":"#ef9a9a","borderColor":"#ef9a9a","pointBackgroundColor":"#ef9a9a","stack":"Stack 1"}]
};
var ctx = document.getElementById("canvas").getContext("2d");
var myBar = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
tooltip: {
enabled: false,
position: 'nearest',
external: function (context) {
// init
const {chart, tooltip} = context;
// remove old tooltip
var container = chart.canvas.parentNode.querySelector('.tooltip');
if (container) {
chart.canvas.parentNode.removeChild(container);
}
// determine if tooltip exists
if (tooltip.opacity === 0) {
return;
}
// group stacks
const groups = {};
tooltip.dataPoints.forEach(function (point) {
if (groups.hasOwnProperty(barChartData.datasets[point.datasetIndex].label)) {
groups[barChartData.datasets[point.datasetIndex].label] += parseFloat(barChartData.datasets[point.datasetIndex].data[point.dataIndex]);
} else {
groups[barChartData.datasets[point.datasetIndex].label] = parseFloat(barChartData.datasets[point.datasetIndex].data[point.dataIndex]);
}
});
// build tooltip rows
var rows = '';
Object.keys(groups).forEach(function (groupName) {
rows += renderTemplate('template-tooltip-row', {
group: groupName,
value: groups[groupName].toLocaleString(undefined, {minimumFractionDigits: 2})
});
});
// build tooltip
chart.canvas.parentNode.insertAdjacentHTML('beforeEnd', renderTemplate('template-tooltip', {
rows: rows,
title: tooltip.title[0]
}));
// position tooltip
const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas;
container = chart.canvas.parentNode.querySelector('.tooltip');
container.style.left = positionX + tooltip.caretX + 'px';
container.style.top = positionY + tooltip.caretY + 'px';
container.style.font = tooltip.options.bodyFont.string;
container.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
}
}
}
}
});
/**
* render html template
* #param {string} templateId - id of html template
* #param {object} templateValues - values for each template placeholder
* #return {string} template content
*/
function renderTemplate(templateId, templateValues) {
var propHandle; // property key
var templateText; // html template content
var templateValue; // value for template placeholder
// get template content, replace each placeholder with value
templateText = document.querySelector('#' + templateId).innerHTML;
if (templateValues) {
for (propHandle in templateValues) {
if (templateValues.hasOwnProperty(propHandle)) {
templateValue = '';
// convert template value to string
if (templateValues[propHandle] !== null) {
if (templateValues[propHandle].hasOwnProperty('results')) {
templateValue = encodeURIComponent(JSON.stringify(templateValues[propHandle].results));
} else {
templateValue = templateValues[propHandle].toString();
}
}
// handle dollar sign in template value
if (templateValue.indexOf('$') > -1) {
templateValue = templateValue.replace(new RegExp('\\$', 'g'), '$$$');
}
// replace template placeholder(s) with template value
if (templateText.indexOf('{{' + propHandle + '}}') > -1) {
templateText = templateText.replace(
new RegExp('{{' + propHandle + '}}', 'g'),
templateValue
);
}
}
}
}
return templateText.trim();
}
});
.align-right {
text-align: right;
}
.table {
border-collapse: separate;
border-spacing: 0vw 0vw;
display: table;
}
.table-body {
display: table-row-group;
}
.table-cell {
display: table-cell;
padding: 4px;
}
.table-foot {
display: table-footer-group;
}
.table-head {
display: table-header-group;
}
.table-row {
display: table-row;
}
.title {
font-weight: bold;
}
.tooltip {
background-color: rgba(0, 0, 0, 0.85);
border-radius: 3px;
color: #ffffff;
pointer-events: none;
position: absolute;
transform: translate(-50%, 0);
transition: all 0.1s ease;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js#3.7.0/dist/chart.min.js"></script>
<canvas id="canvas" height="100"></canvas>
<script id="template-tooltip" type="text/html">
<div class="tooltip">
<div class="title">{{title}}</div>
<div class="table">
<div class="table-body">{{rows}}</div>
</div>
</div>
</script>
<script id="template-tooltip-row" type="text/html">
<div class="table-row">
<div class="table-cell title">{{group}}:</div>
<div class="table-cell align-right">{{value}}</div>
</div>
</script>
I'm in need of a calendar widget and, since I'm using Bootstrap 4 and ng-bootstrap in my current project, I'd like to know if the ng-boostrap widget support the multiselection of dates in some way.
I've already tried the wijmo Calendar for multiselection, but without success. Otherwise, can you recommend me a datepicker widget that has this capability?
This can help
<p>This datepicker uses a custom template to display days.</p>
<ngb-datepicker
[showWeekNumbers]="true"
[dayTemplate]="customDay"
(dateSelect)="selectOne($event)"
></ngb-datepicker>
<ng-template
#customDay
let-date
let-currentMonth="currentMonth"
let-selected="selected"
let-disabled="disabled"
let-focused="focused"
>
<span
class="custom-day"
[class.focused]="focused"
[class.bg-primary]="isSelected(date)"
>{{ date.day }}</span
>
</ng-template>
<div *ngIf="modelList.length>0">
<h1>Selected dates:</h1>
<pre>{{modelList| json}} </pre>
</div>
TypeScript
import { Component } from '#angular/core';
import {
NgbCalendar,
NgbDate,
NgbDateStruct,
} from '#ng-bootstrap/ng-bootstrap';
#Component({
selector: 'ngbd-datepicker-customday',
templateUrl: './datepicker-customday.html',
styles: [
`
.custom-day {
text-align: center;
padding: 0.185rem 0.25rem;
border-radius: 0.25rem;
display: inline-block;
width: 2rem;
}
.custom-day:hover, .custom-day.focused {
background-color: #e6e6e6;
}
.bg-primary {
border-radius: 1rem;
}
`,
],
})
export class NgbdDatepickerCustomday {
model: NgbDateStruct;
modelList: Array<NgbDateStruct> = [];
constructor(private calendar: NgbCalendar) {}
isSelected = (date: NgbDate) => {
return this.modelList.indexOf(date) >= 0;
};
selectOne(date) {
if (this.modelList.indexOf(date) >= 0) {
this.modelList = this.modelList.filter(function (ele) {
return ele != date;
});
} else {
this.modelList.push(date);
}
console.log(this.modelList);
}
}
Demo stackblitz
Yes, the ng-bootstrap datepicker supports range selection. You'll need to do manual conversion between the NgbDateStruct and a JavaScript Date object.