Cannot set scale type to custom scale in ChartJS - charts

I followed the ChartJS documentation for creating a custom scale:
https://www.chartjs.org/docs/master/samples/advanced/derived-axis-type.html
First I defined the custom scale as its own class extending the base Scale interface:
import { Scale, LinearScale, BubbleDataPoint, Chart, ChartTypeRegistry, ScatterDataPoint} from 'chart.js';
export default class SquareRootAxis extends Scale {
static defaults: {} = {};
_startValue: any;
_valueRange: number;
constructor(cfg: { id: string; type: string; ctx: CanvasRenderingContext2D; chart: Chart<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint)[], unknown>; }) {
super(cfg);
this._startValue = undefined;
this._valueRange = 0;
}
parse(raw: any, index: number) {
const value = LinearScale.prototype.parse.apply(this, [raw, index]);
return isFinite(value) && value > 0 ? value : null;
}
determineDataLimits() {
this.min = 0.5
this.max=31
}
buildTicks() {
const userDefinedTicks = [1, 2, 3, 5, 7, 10, 15, 20, 25, 30];
const ticks = [];
for (const tick of userDefinedTicks) {
ticks.push({
value: tick
});
}
return ticks;
}
/**
* #protected]
*/
configure() {
const start = this.min;
super.configure();
this._startValue = Math.pow(start, 1/2);
this._valueRange = Math.pow(this.max, 1/2) - Math.pow(start, 1/2);
}
getPixelForValue(value: number | undefined) {
if (value === undefined ) {
value = this.min;
}
return this.getPixelForDecimal(value === this.min ? 0 :
(Math.pow(value, 1/2) - this._startValue) / this._valueRange);
}
getValueForPixel(pixel: number) {
const decimal = this.getDecimalForPixel(pixel);
return Math.pow(this._startValue + decimal * this._valueRange, 2);
}
}
I then imported the custom scale Class and used it below:
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Scatter } from 'react-chartjs-2';
import SquareRootAxis from './sqaureRootAxis';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
SquareRootAxis
);
SquareRootAxis.id = 'squareRoot';
export function Chart(props: { chartData: ChartData }) {
return <Scatter
data={props.chartData}
options={{
responsive: true,
scales: {
x: {
type: 'squareRoot',
},
},
}}
/>;
}
But when I set my chart to the custom scale name, I get the following TS error:
Type '"squareRoot"' is not assignable to type '"time" | "linear" | "logarithmic" | "category" | "timeseries"'.ts(2322)
index.esm.d.ts(3618, 27): The expected type comes from property 'type' which is declared here on type '_DeepPartialObject<{ type: "time"; } & Omit<CartesianScaleOptions
It seems despite following the docs, the ChartJS scale type only recognizes the built-in scales. What am I doing wrong?

First in the sample code it looks like you have a typo in your import:
import SquareRootAxis from './sqaureRootAxis.
Then it seems you need to register the new scale, which I cannot see in your code, as follows:
chart.register(SquareRootAxis);
For some reason this is commented at the bottom of the code in your link, and is also explained here:
https://www.chartjs.org/docs/latest/developers/axes.html

Related

While horizontal scrolling the datalabels of charjs is overlapping to y-axis

To show all bars I have set the horizontal scrolling to chartjs in Ionic angular project , and i have used the DataLabelsPlugin constant for bars label. and while scrolling the datalabels is overlapping with y-axis its not hiding before y-axis like bars.
and also horizontal scroll is not happening smoothly.
graph working fine as a expected output
marked with issue about - after scrolling the datalabels went over the y-axis not hide below y-axis like bars
I have tried to add and used the custom datalabels but same issue i am getting and i didnt find any css or attribute on 'https://www.chartjs.org/docs/latest/' official site or not on any other sites -> to hide the datalabels from over the y-axis.
ts file code:
createBarChart() {
const footer = (tooltipItems) => {
let sum = 0;
tooltipItems.forEach(function(tooltipItem) {
sum += tooltipItem.parsed.y;
});
return this.util.getFormatValue(sum)+'%';
};
const toolLabel = (tooltipItems) => {
return "";
};
const toolTitle = (tooltipItems) => {
var string_to_array = function (str) {
return str.trim().split("#$#$");
};
var ss;
tooltipItems.forEach(function(tooltipItem) {
ss = string_to_array(tooltipItem.label.replace(/(.{40})/g, "$1#$#$"))
});
return ss;
};
let graphSize = Math.max(...this.daywise_occupancy);
if(graphSize == 0){
graphSize =1;
}
const plugin = {
id: 'customCanvasBackgroundColor',
beforeDraw: (chart, args, options) => {
const {ctx} = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
this.bars = new Chart(this.barchart6.nativeElement, {
type: 'bar',
data: {
labels: this.daywise_date,
datasets: [{
data: this.daywise_occupancy,
backgroundColor: function(context) {
var value = context.dataset.data[context.dataIndex];
return value <= 15 ? '#f95959'
: value > 15 && value <=60 ? '#F5A623'
: '#00ADB5'
},
borderColor: function(context) {
var value = context.dataset.data[context.dataIndex];
return value <= 15 ? '#f95959'
: value > 15 && value <=60 ? '#F5A623'
: '#00ADB5'
},
borderWidth: 1,
barThickness:30,
}]
},
plugins: [DataLabelsPlugin,plugin],
options: {
animations: {
tension: {
duration: 1000,
easing: 'linear',
from: 1,
to: 0,
loop: true
}
},
scales: {
x: {
min:0,
max:5,
ticks : {
maxRotation: 70,
minRotation: 70,
font:{
size:10,
},
callback: function(value : any, index, ticks_array) {
let characterLimit = 12;
let label = this.getLabelForValue(value);
if ( label.length >= characterLimit) {
return label.slice(0, label.length).substring(0, characterLimit -1).trim() + '..';
}
return label;
}
}
},
y: { // defining min and max so hiding the dataset does not change scale range
min: 0,
max: this.loader.getGraphsizeRound(graphSize),
title: { display: true, text: (this.titleSet)? '% of Branches Contribution' : '% of Seat Occupancy' },
beginAtZero: true,
display: true,
position: 'left',
// ticks: {
// stepSize: 6,
// },
}
},
plugins: {
legend: {
display: false
},
datalabels:{
anchor: 'end',
align: 'end',labels: {
value: {
color: '#2C3A45;',
formatter: function (value) {
// return Math.round(value) + '%';
return value + '%';
},
font:{
weight:700,
size:14
}
}
}
},
tooltip: {
callbacks: {
footer:footer,
label: toolLabel,
title:toolTitle
},
displayColors:false
}
}
}
});
this.bars.canvas.addEventListener('touchmove',(eve) => {
this.touchmove(eve,this.bars)
});
this.bars.canvas.addEventListener('touchstart',(eve) => {
this.touchstart(eve)
});
}
touchstart(e)
{
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
}
touchmove(e,chart)
{
var deltaX = e.touches[0].clientX - this.startX,
deltaY = e.touches[0].clientY - this.startY;
const dataLength = chart.data.labels.length;
let min = chart.options.scales.x.min;
if(deltaX < 0){
if( chart.options.scales.x.max >= dataLength ){
chart.options.scales.x.min = dataLength - 5;
chart.options.scales.x.max = dataLength;
}else{
chart.options.scales.x.min += 1;
chart.options.scales.x.max += 1;
}
// console.log( chart.options.scales.x.min);
// chart1line.options.scales.y.max = graphSize
}else if(deltaX > 0){
if( chart.options.scales.x.min <= 0 ){
chart.options.scales.x.min = 0;
chart.options.scales.x.max = 4;
}else{
chart.options.scales.x.min -= 1;
chart.options.scales.x.max -= 1;
}
}else{
}
chart.update();
}
HTML code:
<div class="chartWrapper">
<div class="chartAreaWrapper">
<canvas #barchart6 height="190" max-height="190" width="0"></canvas>
</div>
</div>
My expected output
horizontal scroll work smoothly.
after scrolling label should not overlap on y-axis.

Donut chart with custom tooltip using Chartist in React

I am creating a donut chart using Chartist but unable to add a custom tooltip with resize slice on the mouse hover event. Chartist "Draw" method is rerender on changing the position of mouse on hover of the chart
I pasted my code as below:
/* eslint-disable no-unused-vars */
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import ChartistGraph from 'react-chartist'
import Chartist from 'chartist'
import styles from './MyChart.scss'
import {formatNumber} from '../../../../utils/formatter'
import MyChartInfo from './MyChartInfo'
function MyChart ({ dataSeries, getDetailsTable }) {
const [changePercent, setChangePercent] = useState(0)
const [showToolTip, setShowToolTip] = useState(false)
const [myChartInfoDetails, setMyChartInfoDetails] = useState({sectorName: '', changePercent: ''})
const [toolTipPosition, setToolTipPosition] = useState({})
function setToolTip () {
const PopOverData = [myChartInfoDetails.sectorName || '', `% Return: ${myChartInfoDetails.changePercent || ''}`, '(Click to view top & worst performing equities)']
return (<MyChartInfo dataTable={PopOverData} style={toolTipPosition} />)
}
let pieOptions = {
width: '520px',
height: '520px',
donut: true,
donutWidth: 150,
donutSolid: false,
startAngle: 270,
showLabel: true,
chartPadding: 60,
labelOffset: 105,
labelDirection: 'explode',
labelInterpolationFnc: function (value) {
return value
}
}
let pieData = {
series: dataSeries && dataSeries.length > 0 ? dataSeries.map(item => Math.abs(item.value)) : [100],
labels: dataSeries && dataSeries.length > 0 ? dataSeries.map(item => item.name.concat('<br>', formatNumber(item.value, {precision: 2, negSign: true, posSign: true}))) : ['']
}
// eslint-disable-next-line no-unused-vars
const listner = {
created: (chart) => {
const chartPie = document.querySelectorAll('.ct-chart-donut .ct-slice-donut')
chartPie && chartPie.forEach((pie) => {
pie.addEventListener('mouseenter', (e) => {
e.stopPropagation()
pie.setAttribute('style', 'stroke-width: 170px;')
console.log('hover aa ', changePercent, Math.abs(pie.getAttribute('ct:value')))
setChangePercent(Math.abs(pie.getAttribute('ct:value')))
let topPosition = e.layerY + 30
let leftPosition = e.layerX + 30
setToolTipPosition({top: topPosition, left: leftPosition})
const myChartData = dataSeries.find(sector => Math.abs(sector.value) === Math.abs(pie.getAttribute('ct:value')))
setSectorDetails({sectorName: myChartData.name, changePercent: myChartData.value})
setShowToolTip(true)
})
pie.addEventListener('mouseleave', (e) => {
pie.setAttribute('style', 'stroke-width: 150px;')
setShowToolTip(false)
})
pie.addEventListener('click', (e) => {
const Id = dataSeries.find(sector => Math.abs(sector.value) === Math.abs(pie.getAttribute('ct:value'))).id.toString()
console.log('click ', Id, pie.getAttribute('ct:value'))
getDetailsTable(Id)
})
})
},
draw: (data) => {
// console.log('data', data)
if (data.type === 'slice') {
// Get the total path length in order to use for dash array animation
var pathLength = data.element._node.getTotalLength()
// Set a dasharray that matches the path length as prerequisite to animate dashoffset
data.element.attr({
'stroke-dasharray': pathLength + 'px ' + pathLength + 'px'
})
let noOfNonZeros = dataSeries.length > 0 && dataSeries.filter(e => e.value > 0).length
// Create animation definition while also assigning an ID to the animation for later sync usage
var animationDefinition = {
'stroke-dashoffset': {
id: 'anim' + data.index,
dur: 1,
from: -pathLength + 'px',
to: noOfNonZeros > 1 ? '2px' : '0px',
easing: Chartist.Svg.Easing.easeOutQuint,
// We need to use `fill: 'freeze'` otherwise our animation will fall back to initial (not visible)
fill: 'freeze'
}
}
// If this was not the first slice, we need to time the animation so that it uses the end sync event of the previous animation
if (data.index !== 0) {
animationDefinition['stroke-dashoffset'].begin = 'anim' + (data.index - 1) + '.end'
}
// We need to set an initial value before the animation starts as we are not in guided mode which would do that for us
data.element.attr({
'stroke-dashoffset': -pathLength + 'px'
})
// We can't use guided mode as the animations need to rely on setting begin manually
// See http://gionkunz.github.io/chartist-js/api-documentation.html#chartistsvg-function-animate
data.element.animate(animationDefinition, false)
if (dataSeries.length === 0) {
data.element._node.setAttribute('data-value', 'no-composition')
}
}
if (data.type === 'label') {
let textHtml = ['<p>', data.text, '</p>'].join('')
let multilineText = Chartist.Svg('svg').foreignObject(
textHtml,
{
style: 'overflow: visible;',
x: data.x - 30,
y: data.y - 30,
width: 90,
height: 30
},
'ct-label'
)
data.element.replace(multilineText)
}
}
}
return (
<div data-testid='gPieChart' id='performance_chart'>
{<ChartistGraph
data={pieData}
options={pieOptions}
type='Pie'
style={{ width: '100%', height: '100%' }}
className={styles.MyChart}
listener={listner}
/>
}
{showToolTip && setToolTip()}
</div>
)
}
MyChart.propTypes = {
dataSeries: PropTypes.array,
getDetailsTable: PropTypes.func
}
export default MyChart
Output
I want to add a custom tooltip with resize slice functionality on mouse hover event

Uncaught TypeError: _arc2.default.GreatCircle is not a constructor

Trying to create plugin for react-leaflet. I am getting this error while
i am returning
L.Polyline.Arc(position.from, position.to, option)
This is my Component
import React, { PropTypes } from 'react';
import { Path } from 'react-leaflet';
import L from 'leaflet'
import { Arc } from './leaflet.arc';
export default class ArcLine extends Path {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
option:PropTypes.object,
position: PropTypes.object.isRequired
};
createLeafletElement (props) {
const { position, option, ...options } = props
return L.Polyline.Arc(position.from, position.to, option)
}
updateLeafletElement (fromProps, toProps) {
if (toProps.position !== fromProps.position) {
this.leafletElement._createLatLngs(line, toProps.position.from)
}
this.setStyleIfChanged(fromProps, toProps)
}
}
./leaflet.arc is
import arc from 'arc';
/**
* Transform L.LatLng to {x, y} object
* #param {L.LatLng} latlng
* #returns {{x: {number}, y: {number}}}
* #private
*/
const _latLngToXY = latlng => ({
x: latlng.lng,
y: latlng.lat
});
/**
* Create array of L.LatLng objects from line produced by arc.js
* #param {object} line
* #param {L.LatLng} from
* #private
* #returns {Array}
*/
function _createLatLngs(line, from) {
if (line.geometries[0] && line.geometries[0].coords[0]) {
/**
* stores how many times arc is broken over 180 longitude
* #type {number}
*/
let wrap = from.lng - line.geometries[0].coords[0][0] - 360;
return line.geometries
.map(subLine => {
wrap += 360;
return subLine.coords.map(point => L.latLng([point[1], point[0] + wrap]));
})
.reduce((all, latlngs) => all.concat(latlngs));
} else {
return [];
}
}
if (!L) {
throw "Leaflet.js not included";
} else if (!arc && !arc.GreatCircle) {
throw "arc.js not included";
} else {
L.Polyline.Arc = function (from, to, options) {
from = L.latLng(from);
to = L.latLng(to);
debugger
var vertices = 10;
var arcOptions = {};
if (options) {
if (options.offset) {
arcOptions.offset = options.offset;
delete options.offset;
}
if (options.vertices) {
vertices = options.vertices;
delete options.vertices;
}
}
var generator = new arc.GreatCircle({x: from.lng, y: from.lat}, {x: to.lng, y: to.lat});
var line = generator.Arc(vertices, arcOptions);
var latLngs = [];
var wrap = 0; // counts how many times arc is broken over 180 degree
if (line.geometries[0] && line.geometries[0].coords[0])
wrap = from.lng - line.geometries[0].coords[0][0];
line.geometries.forEach(function(line) {
line.coords.forEach(function (point) {
latLngs.push(L.latLng(
[point[1], point[0] + wrap]
));
});
wrap += 360;
});
line.geometries[0].coords
return L.polyline(latLngs, options);
};
}
Why am i getting this error, any suggestion?
In this line here:
import { Arc } from './leaflet.arc'
...you are trying to import the Arc variable from your leaflet.arc file. However, in your leaflet.arc file it doesn't look like you are exporting anything, simply attaching the Arc function to Leaflet's Polyline object. Also, in the code in your component you are not even using the Arc variable that you are trying to import. So, the code fails because a property is trying to be read from the undefined export from leaflet.arc. It seems that you should probably just import the file without trying to import any objects.
TLDR:
Replace this line:
import { Arc } from './leaflet.arc'
with this:
import './leaflet.arc'

Center Highstock chart scrollbar handle

I have a bunch of stock charts rendered with Highstock charting API.
In an attempt to center the scrollbar handle for each chart, I use the following piece of code:
/* ============ Position chart scroll BEGIN ============ */
$(".highcharts-container").each(function () {
var scrollBar = $(this).find('.highcharts-scrollbar');
var scrollBarElms = scrollBar.find('rect');
var scrollBarTrackWidth = $(scrollBarElms[0]).attr("width");
var scrollBarHandleWidth = $(scrollBarElms[1]).attr("width");
var xPos = (scrollBarTrackWidth / 2) - (scrollBarHandleWidth / 2);
$(scrollBarElms[1]).attr("x", xPos);
});
/* ============ Position chart scroll END ============ */
The problem is that the handle and the 3 vertical lines that should 'decorate' it are separated. (You can see the entire thing HERE.)
Any suggestions on how to keep them together?
function getData() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -999; i <= 0; i = i + 1) {
data.push([
time + i * 1000,
Math.round(Math.random() * 100)]);
}
return data;
}
function getRange(data) {
var l = data.length,
range = l * 0.1, // number of points -> 10%
min = data[Math.round(l / 2 - range / 2)][0],
max = data[Math.round(l / 2 + range / 2)][0];
return {
min: min,
max: max
};
}
/* ============ CHARTS OPTIONS BEGIN ============ */
var options = {
chart: {
zoomType: 'x',
events: {
load: function () {
// set up the updating of the chart each second
var series = this.series[0];
setInterval(function () {
var x = (new Date()).getTime();
var y = Math.round(Math.random() * 100);
series.addPoint([x, y]);
}, 1000);
}
}
},
xAxis: {
},
rangeSelector: {
buttons: [{
count: 1,
type: 'minute',
text: '1M'
}, {
count: 5,
type: 'minute',
text: '5M'
}, {
type: 'all',
text: 'All'
}],
inputEnabled: false,
selected: 0
},
title: {
text: null
},
exporting: {
enabled: false
},
// Disable navigator
navigator: {
enabled: false
},
series: [{
name: ''
}]
}
/* ============ CHARTS OPTIONS END ============ */
/* ============ DRAW CHARTS BEGIN ============ */
function renderCharts() {
$('div.chart-container').each(function () {
var chartId = $(this).attr('id');
var chartIdParts = chartId.split('-');
var chartIdentifier = chartIdParts[1];
//Set chart options dinamically
var chartId = "chart" + chartIdentifier;
var chart = $('#' + chartId);
var renderTo = "chartcontainer-" + chartIdentifier;
//Render Charts for each aech container
options.chart.renderTo = renderTo;
options.chart.type = 'line';
options.series[0].data = getData();
var range = getRange(options.series[0].data);
options.xAxis.min = range.min;
options.xAxis.max = range.max;
var chart = new Highcharts.StockChart($.extend(true, {}, options));
});
}
function setChatType() {
// Show types list (piker)
$('.current-type').on('click', function () {
$(this).parents('div.chart-options').find('ul.type ul').addClass('clicked');
});
$('.chart-options ul ul li a').on('click', function () {
//Get piked chart type
var type = $(this).parent('li').attr('data-chart-type');
// For text and Title Capitalization
var textAndTitle = type.replace(/^[a-z]/, function (m) {
return m.toUpperCase()
});
// Show piked type in picker
var currSetClass = 'current-type ' + type;
$(this).parents('.chart-options').find('.current-type')
.text(textAndTitle)
.attr({
class: currSetClass,
title: textAndTitle
});
// Then Hide the types list
$('.chart-options ul ul').removeClass('clicked');
//Identify current chart container by ID
var chartCtnId = $(this).parents('div.chart').find('.chart-container').attr('id');
// Render chart again with new type
options.chart.renderTo = chartCtnId;
options.chart.type = type;
var chart = new Highcharts.StockChart($.extend(true, {}, options));
});
}
/* ============ DRAW CHARTS END ============ */
$(document).ready(function () {
$("article.grid:even").addClass('left')
$("article.grid:odd").addClass('right');
// Draw charts
renderCharts();
// Set/change chart type
setChatType();
});
The entire solution can be found HERE.
Thanks to Paweł Fus for help!

Functions Overloading in interface and classes - how to?

I have this interface :
interface IPoint {
getDist(): string;
getDist(x: number): any;
}
and I need a class to implement it but I can't get the right syntax to implement the getDist() method
in the class..
class Point implements IPoint {
// Constructor
constructor (public x: number, public y: number) { }
pointMethod() { }
getDist() {
Math.sqrt(this.x * this.x + this.y * this.y);
}
// Static member
static origin = new Point(0, 0);
}
it says:
Class 'Point' declares interface 'IPoint' but does not implement it:
Types of property 'getDist' of types 'Point' and 'IPoint' are
incompatible:Call signatures of types '() => void' and '{ (): string;
(x: number): any; }' are incompatible
What's the proper way to do this?
Thanks
When you declare the function in the class you need to decorate it with the overloads:
getDist(): string;
getDist(x: number): any;
getDist(x?: number): any {
// your code
}
This answer describes how to implement method overloading in TypeScript, and it's not pretty:
interface IPoint {
getDist(): string;
getDist(x: number): any;
}
class Point implements IPoint {
// Constructor
constructor (public x: number, public y: number) { }
pointMethod() { }
getDist(x?: number) {
if (x && typeof x == "number") {
return 'foo';
} else {
return 'bar';
}
}
}
N.B. with the particular combination of declared return types in the interface, you are limited to returning strings from getDist.
also you can use the default value
interface Foo{
next()
next(steps: number)
prev()
prev(steps: number)
}
next(steps: number = 1) {
// ...
}
prev(steps: number = 1) {
// ...
}
The following is a variation on some of the above
class Position {
x: number;
y: number;
constructor(x : number = 0, y : number = 0) {
this.x = x;
this.y = y;
}
}
class Pen {
colour: string;
constructor(colour: string) {
this.colour = colour;
}
}
class PlottingHead {
isPenUp: boolean;
pen: Pen;
position: Position;
constructor() {
this.penUp();
}
move(p: Position): void;
move(x: number, y: number): void;
move(x: number | Position, y?: number): void {
if (typeof x === "number")
{
x = new Position(x, y);
}
this.penUp();
this.position = x;
}
draw(x: number | Position, y?: number): void {
if (typeof x === "number")
{
x = new Position(x, y);
}
this.penDown();
this.position = x;
}
penDown(): void {
this.isPenUp = false;
}
penUp(): void {
this.isPenUp = true;
}
onChangePen(newPen: Pen) {
this.penUp();
this.pen = newPen;
}
}
The move function takes either a single position object or a pair of numeric values. This can obviously be extended as required.