diff --git a/app/assets/javascripts/admin/addon/components/admin-graph.js b/app/assets/javascripts/admin/addon/components/admin-graph.js index b6bf870bc19..8df160aa853 100644 --- a/app/assets/javascripts/admin/addon/components/admin-graph.js +++ b/app/assets/javascripts/admin/addon/components/admin-graph.js @@ -27,14 +27,16 @@ export default Component.extend({ data: data, options: { responsive: true, - tooltips: { - callbacks: { - title: (context) => - moment(context[0].xLabel, "YYYY-MM-DD").format("LL"), + plugins: { + tooltip: { + callbacks: { + title: (context) => + moment(context[0].label, "YYYY-MM-DD").format("LL"), + }, }, }, scales: { - yAxes: [ + y: [ { display: true, ticks: { diff --git a/app/assets/javascripts/admin/addon/components/admin-report-chart.js b/app/assets/javascripts/admin/addon/components/admin-report-chart.js index 9e2d382fc98..7711ca8d3dc 100644 --- a/app/assets/javascripts/admin/addon/components/admin-report-chart.js +++ b/app/assets/javascripts/admin/addon/components/admin-report-chart.js @@ -112,14 +112,16 @@ export default Component.extend({ type: "line", data, options: { - tooltips: { - callbacks: { - title: (tooltipItem) => - moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"), + plugins: { + tooltip: { + callbacks: { + title: (tooltipItem) => + moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"), + }, + }, + legend: { + display: false, }, - }, - legend: { - display: false, }, responsive: true, maintainAspectRatio: false, @@ -136,15 +138,10 @@ export default Component.extend({ }, }, scales: { - yAxes: [ + y: [ { display: true, ticks: { - userCallback: (label) => { - if (Math.floor(label) === label) { - return label; - } - }, callback: (label) => number(label), sampleSize: 5, maxRotation: 25, @@ -152,7 +149,7 @@ export default Component.extend({ }, }, ], - xAxes: [ + x: [ { display: true, gridLines: { display: false }, diff --git a/app/assets/javascripts/admin/addon/components/admin-report-stacked-chart.js b/app/assets/javascripts/admin/addon/components/admin-report-stacked-chart.js index 4c3a9bb6330..d94d6c308c0 100644 --- a/app/assets/javascripts/admin/addon/components/admin-report-stacked-chart.js +++ b/app/assets/javascripts/admin/addon/components/admin-report-stacked-chart.js @@ -89,21 +89,24 @@ export default Component.extend({ animation: { duration: 0, }, - tooltips: { - mode: "index", - intersect: false, - callbacks: { - beforeFooter: (tooltipItem) => { - let total = 0; - tooltipItem.forEach( - (item) => (total += parseInt(item.yLabel || 0, 10)) - ); - return `= ${total}`; + plugins: { + tooltip: { + mode: "index", + intersect: false, + callbacks: { + beforeFooter: (tooltipItem) => { + let total = 0; + tooltipItem.forEach( + (item) => (total += parseInt(item.parsed.y || 0, 10)) + ); + return `= ${total}`; + }, + title: (tooltipItem) => + moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"), }, - title: (tooltipItem) => - moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"), }, }, + layout: { padding: { left: 0, @@ -113,16 +116,11 @@ export default Component.extend({ }, }, scales: { - yAxes: [ + y: [ { stacked: true, display: true, ticks: { - userCallback: (label) => { - if (Math.floor(label) === label) { - return label; - } - }, callback: (label) => number(label), sampleSize: 5, maxRotation: 25, @@ -130,8 +128,7 @@ export default Component.extend({ }, }, ], - - xAxes: [ + x: [ { display: true, gridLines: { display: false }, diff --git a/app/assets/javascripts/discourse/app/lib/public-js-versions.js b/app/assets/javascripts/discourse/app/lib/public-js-versions.js index 8ec95682a3d..f3bb8cde403 100644 --- a/app/assets/javascripts/discourse/app/lib/public-js-versions.js +++ b/app/assets/javascripts/discourse/app/lib/public-js-versions.js @@ -4,9 +4,9 @@ export const PUBLIC_JS_VERSIONS = { "ace/ace.js": "ace.js/1.4.12/ace.js", "jsoneditor.js": "@json-editor/json-editor/2.5.2/jsoneditor.js", - "Chart.min.js": "chart.js/2.9.4/Chart.min.js", + "Chart.min.js": "chart.js/3.5.1/Chart.min.js", "chartjs-plugin-datalabels.min.js": - "chartjs-plugin-datalabels/0.7.0/chartjs-plugin-datalabels.min.js", + "chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.min.js", "diffhtml.min.js": "diffhtml/1.0.0-beta.18/diffhtml.min.js", "jquery.magnific-popup.min.js": "magnific-popup/1.1.0/jquery.magnific-popup.min.js", diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-search-log-term-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-search-log-term-test.js index a1d472ae881..078676b9530 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-search-log-term-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-search-log-term-test.js @@ -9,7 +9,7 @@ acceptance("Admin - Search Log Term", function (needs) { await visit("/admin/logs/search_logs/term?term=ruby"); assert.ok(exists(".search-logs-filter"), "has the search type filter"); - assert.ok(exists("canvas.chartjs-render-monitor"), "has graph canvas"); + assert.ok(exists("canvas"), "has graph canvas"); assert.ok(exists("div.header-search-results"), "has header search results"); }); }); diff --git a/package.json b/package.json index abcdfe65f98..6f3994f4ea8 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "blueimp-file-upload": "10.13.0", "bootbox": "3.2.0", "bootstrap": "v3.4.1", - "chart.js": "2.9.4", - "chartjs-plugin-datalabels": "^0.7.0", + "chart.js": "3.5.1", + "chartjs-plugin-datalabels": "^2.0.0", "diffhtml": "^1.0.0-beta.18", "eslint-config-discourse": "^1.1.8", "handlebars": "^4.7.7", diff --git a/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6 b/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6 index be7826602d4..20ec3b22b07 100644 --- a/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6 +++ b/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6 @@ -5,6 +5,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { getColors } from "discourse/plugins/poll/lib/chart-colors"; import { htmlSafe } from "@ember/template"; import { mapBy } from "@ember/object/computed"; +import { next } from "@ember/runloop"; export default Component.extend({ // Arguments: @@ -92,6 +93,7 @@ export default Component.extend({ }, options: { plugins: { + tooltip: false, datalabels: { color: "#333", backgroundColor: "rgba(255, 255, 255, 0.5)", @@ -122,10 +124,14 @@ export default Component.extend({ responsive: true, aspectRatio: 1.1, animation: { duration: 0 }, - tooltips: false, + + // wrapping setHighlightedOption in next block as hover can create many events + // prevents two sets to happen in the same computation onHover: (event, activeElements) => { if (!activeElements.length) { - this.setHighlightedOption(null); + next(() => { + this.setHighlightedOption(null); + }); return; } @@ -137,7 +143,9 @@ export default Component.extend({ // Clear the array to avoid issues in Chart.js activeElements.length = 0; - this.setHighlightedOption(Number(optionIndex)); + next(() => { + this.setHighlightedOption(Number(optionIndex)); + }); }, }, }; diff --git a/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6 index 66c160f2f29..794efa0de3f 100644 --- a/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6 +++ b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6 @@ -47,7 +47,6 @@ export default Controller.extend(ModalFunctionality, { loadScript("/javascripts/Chart.min.js") .then(() => loadScript("/javascripts/chartjs-plugin-datalabels.min.js")) .then(() => { - window.Chart.plugins.unregister(window.ChartDataLabels); this.fetchGroupedPollData(); }); }, diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 index c74e162cd37..8a4a69a02e0 100644 --- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 +++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 @@ -465,17 +465,19 @@ createWidget("discourse-poll-pie-canvas", { loadScript("/javascripts/Chart.min.js").then(() => { const data = attrs.poll.options.mapBy("votes"); const labels = attrs.poll.options.mapBy("html"); - const config = pieChartConfig(data, labels); + const config = pieChartConfig(data, labels, { + legendContainerId: `poll-results-legend-${attrs.id}`, + }); const el = document.getElementById(`poll-results-chart-${attrs.id}`); - // eslint-disable-next-line - let chart = new Chart(el.getContext("2d"), config); - document.getElementById( - `poll-results-legend-${attrs.id}` - ).innerHTML = chart.generateLegend(); + this._chart = new Chart(el.getContext("2d"), config); }); }, + willRerenderWidget() { + this._chart?.destroy(); + }, + buildAttributes(attrs) { return { id: `poll-results-chart-${attrs.id}`, @@ -497,15 +499,54 @@ createWidget("discourse-poll-pie-chart", { const chart = this.attach("discourse-poll-pie-canvas", attrs); contents.push(chart); - contents.push(h(`div#poll-results-legend-${attrs.id}.pie-chart-legends`)); + contents.push(h(`ul#poll-results-legend-${attrs.id}.pie-chart-legends`)); return contents; }, }); +const htmlLegendPlugin = { + id: "htmlLegend", + + afterUpdate(chart, args, options) { + const ul = document.getElementById(options.containerID); + ul.innerHTML = ""; + + const items = chart.options.plugins.legend.labels.generateLabels(chart); + items.forEach((item) => { + const li = document.createElement("li"); + li.classList.add("legend"); + li.onclick = () => { + chart.toggleDataVisibility(item.index); + chart.update(); + }; + + const boxSpan = document.createElement("span"); + boxSpan.classList.add("swatch"); + boxSpan.style.background = item.fillStyle; + + const textContainer = document.createElement("span"); + textContainer.style.color = item.fontColor; + textContainer.innerHTML = item.text; + + if (!chart.getDataVisibility(item.index)) { + li.style.opacity = 0.2; + } else { + li.style.opacity = 1.0; + } + + li.appendChild(boxSpan); + li.appendChild(textContainer); + + ul.appendChild(li); + }); + }, +}; + function pieChartConfig(data, labels, opts = {}) { const aspectRatio = "aspectRatio" in opts ? opts.aspectRatio : 2.2; const strippedLabels = labels.map((l) => stripHtml(l)); + return { type: PIE_CHART_TYPE, data: { @@ -517,18 +558,29 @@ function pieChartConfig(data, labels, opts = {}) { ], labels: strippedLabels, }, + plugins: [htmlLegendPlugin], options: { responsive: true, aspectRatio, animation: { duration: 0 }, - legend: { display: false }, - legendCallback: function (chart) { - let legends = ""; - for (let i = 0; i < labels.length; i++) { - legends += `
=0;--u)if(!m()){i.updateRangeFromParsed(c,t,g,l);break}return c}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let n,o,s;for(n=0,o=e.length;nNt(t,r,l,!0)?1:Math.max(e,e*i,n,n*i),g=(t,e,n)=>Nt(t,r,l,!0)?-1:Math.min(e,e*i,n,n*i),p=f(0,c,d),m=f(Mt,h,u),x=g(bt,c,d),b=g(bt+Mt,h,u);n=(p-x)/2,o=(m-b)/2,s=-(p+x)/2,a=-(m+b)/2}return{ratioX:n,ratioY:o,offsetX:s,offsetY:a}}(d,h,l),m=(n.width-a)/u,x=(n.height-a)/f,b=Math.max(Math.min(m,x)/2,0),_=Z(e.options.radius,b),y=(_-Math.max(_*l,0))/e._getVisibleDatasetWeightTotal();e.offsetX=g*_,e.offsetY=p*_,o.total=e.calculateTotal(),e.outerRadius=_-y*e._getRingWeightOffset(e.index),e.innerRadius=Math.max(e.outerRadius-y*c,0),e.updateElements(s,0,s.length,t)}_circumference(t,e){const i=this,n=i.options,o=i._cachedMeta,s=i._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]||o.data[t].hidden?0:i.calculateCircumference(o._parsed[t]*s/_t)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=a.chartArea,l=a.options.animation,c=(r.left+r.right)/2,h=(r.top+r.bottom)/2,d=s&&l.animateScale,u=d?0:o.innerRadius,f=d?0:o.outerRadius,g=o.resolveDataElementOptions(e,n),p=o.getSharedOptions(g),m=o.includeOptions(n,p);let x,b=o._getRotation();for(x=0;x"spacing"!==t,_indexable:t=>"spacing"!==t},yo.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return Y(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class vo extends wn{initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this,i=e._cachedMeta,{dataset:n,data:o=[],_dataset:s}=i,a=e.chart._animationsDisabled;let{start:r,count:l}=function(t,e,i){const n=e.length;let o=0,s=n;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(o=Ht(Math.min(se(r,a.axis,c).lo,i?n:se(e,l,a.getPixelForValue(c)).lo),0,n-1)),s=u?Ht(Math.max(se(r,a.axis,h).hi+1,i?0:se(e,l,a.getPixelForValue(h)).hi+1),o,n)-o:n-o}return{start:o,count:s}}(i,o,a);e._drawStart=r,e._drawCount=l,function(t){const{xScale:e,yScale:i,_scaleRanges:n}=t,o={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!n)return t._scaleRanges=o,!0;const s=n.xmin!==e.min||n.xmax!==e.max||n.ymin!==i.min||n.ymax!==i.max;return Object.assign(n,o),s}(i)&&(r=0,l=o.length),n._datasetIndex=e.index,n._decimated=!!s._decimated,n.points=o;const c=e.resolveDatasetElementOptions(t);e.options.showLine||(c.borderWidth=0),c.segment=e.options.segment,e.updateElement(n,void 0,{animated:!a,options:c},t),e.updateElements(o,r,l,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{iScale:a,vScale:r,_stacked:l}=o._cachedMeta,c=o.resolveDataElementOptions(e,n),h=o.getSharedOptions(c),d=o.includeOptions(n,h),u=a.axis,f=r.axis,g=o.options.spanGaps,p=Tt(g)?g:Number.POSITIVE_INFINITY,m=o.chart._animationsDisabled||s||"none"===n;let x=e>0&&o.getParsed(e-1);for(let c=e;c0!==t))?(t.beginPath(),t.fillStyle=o.multiKeyBackground,ne(t,{x:e,y:g,w:c,h:l,radius:s}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),ne(t,{x:i,y:g+1,w:c-2,h:l-2,radius:s}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(e,g,c,l),t.strokeRect(e,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,c-2,l-2))}t.fillStyle=s.labelTextColors[i]}drawBody(t,e,i){const n=this,{body:o}=n,{bodySpacing:s,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c}=i,h=Ve(i.bodyFont);let d=h.lineHeight,u=0;const f=Ti(i.rtl,n.x,n.width),g=function(i){e.fillText(i,f.x(t.x+u),t.y+d/2),t.y+=d+s},p=f.textAlign(a);let m,x,b,_,y,v,w;for(e.textAlign=a,e.textBaseline="middle",e.font=h.string,t.x=Es(n,p,i),e.fillStyle=i.bodyColor,J(n.beforeBody,g),u=r&&"right"!==p?"center"===a?c/2+1:c+2:0,_=0,v=o.length;_0?i:null;this._zero=!0}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?Math.max(0,e):null,t.max=X(i)?Math.max(0,i):null,t.options.beginAtZero&&(t._zero=!0),t.handleTickRangeOptions()}handleTickRangeOptions(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let n=t.min,o=t.max;const s=t=>n=e?n:t,a=t=>o=i?o:t,r=(t,e)=>Math.pow(10,Math.floor(Pt(t))+e);n===o&&(n<=0?(s(1),a(10)):(s(r(n,-1)),a(r(o,1)))),n<=0&&s(r(o,-1)),o<=0&&a(r(n,1)),t._zero&&t.min!==t._suggestedMin&&n===r(t.min,0)&&s(r(n,-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.options,i=function(t,e){const i=Math.floor(Pt(e.max)),n=Math.ceil(e.max/Math.pow(10,i)),o=[];let s=q(t.min,Math.pow(10,Math.floor(Pt(e.min)))),a=Math.floor(Pt(s)),r=Math.floor(s/Math.pow(10,a)),l=a<0?Math.pow(10,Math.abs(a)):1;do{o.push({value:s,major:Ys(s)}),++r,10===r&&(r=1,++a,l=a>=0?1:l),s=Math.round(r*Math.pow(10,a)*l)/l}while(ao?{start:e-i,end:e}:{start:e,end:e+i}}function Ks(t){const e={l:0,r:t.width,t:0,b:t.height-t.paddingTop},i={},n=[],o=[],s=t.getLabels().length;for(let c=0;ce.r&&(e.r=p.end,i.r=f),m.start=0&&(e[l].major=!0);return e}(t,n,o,i):n}class ca extends En{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e){const i=t.time||(t.time={}),n=this._adapter=new co._date(t.adapters.date);st(i.displayFormats,n.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:sa(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this,e=t.options,i=t._adapter,n=e.time.unit||"day";let{min:o,max:s,minDefined:a,maxDefined:r}=t.getUserBounds();function l(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),r||isNaN(t.max)||(s=Math.max(s,t.max))}a&&r||(l(t._getLabelBounds()),"ticks"===e.bounds&&"labels"===e.ticks.source||l(t.getMinMax(!1))),o=X(o)&&!isNaN(o)?o:+i.startOf(Date.now(),n),s=X(s)&&!isNaN(s)?s:+i.endOf(Date.now(),n)+1,t.min=Math.min(o,s-1),t.max=Math.max(o+1,s)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this,e=t.options,i=e.time,n=e.ticks,o="labels"===n.source?t.getLabelTimestamps():t._generate();"ticks"===e.bounds&&o.length&&(t.min=t._userMin||o[0],t.max=t._userMax||o[o.length-1]);const s=t.min,a=re(o,s,t.max);return t._unit=i.unit||(n.autoSkip?aa(i.minUnit,t.min,t.max,t._getLabelCapacity(s)):function(t,e,i,n,o){for(let s=na.length-1;s>=na.indexOf(i);s--){const i=na[s];if(ia[i].common&&t._adapter.diff(o,n,i)>=e-1)return i}return na[i?na.indexOf(i):0]}(t,a.length,i.minUnit,t.min,t.max)),t._majorUnit=n.major.enabled&&"year"!==t._unit?function(t){for(let e=na.indexOf(t)+1,i=na.length;e1e5*r)throw new Error(i+" and "+n+" are too far apart with stepSize of "+r+" "+a);const g="data"===o.ticks.source&&t.getDataTimestamps();for(d=f,u=0;d