I an using HighCharts HighStock in react-native app and I am trying to get the current visible data of the chart to refresh tooltip with the last visible data, I am already able to have the last value on load since it is the last value of data array but I am failing to get the value after scrolling.
events: {
render() {
const chart = this;
points = [];
Highcharts.each(chart.series, (s) => {
if (s.visible) {
const lastPoint = s.points[s.points.length - 1];
if (lastPoint.y === null) lastPoint.y = 0;
points.push(lastPoint);
}
});
chart.tooltip.refresh(points);
},
You can use Highcharts.find method and check isInside flag in points. If you want to avoid points from a navigator, check isInternal flag in series. You should also reset the state of previously hovered points:
chart: {
events: {
render() {
const chart = this,
lastPoints = chart.lastPoints,
points = [];
if (lastPoints && lastPoints.length) {
lastPoints.forEach(function(lastPoint) {
lastPoint.setState('');
});
}
Highcharts.each(chart.series, (s) => {
if (s.visible && !s.options.isInternal) {
points.push(Highcharts.find(
s.points.slice().reverse(),
function(p) {
return p.isInside ? true : false;
}));
}
});
chart.lastPoints = points;
chart.tooltip.refresh(points);
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/xwgs13r6/
API Reference:
https://api.highcharts.com/class-reference/Highcharts.Point#setState
https://api.highcharts.com/class-reference/Highcharts#.find
Related
I have the logic of disposing of lines in my chart and have a custom cursor that I get from this link and when I dispose the line the label shouldn't show too and it works, but after restoring the rowY the names in textbox is pale, look at the next screenshots pale labels image, normal labels before disposing
rowsY.map((rowY, i) => {
this.seriesInstances[i][1].isDisposed() ? rowY.dispose() : rowY.restore();
if (nearestDataPoints[i]?.location?.y) {
rowY.setText(`${this.seriesInstances[i][1].getName()}: ${+this.chartInstance.getDefaultAxisY().formatValue(nearestDataPoints[i].location.y)} ${this.seriesInitialData[i].unit}`)
}
});
It seems you dispose/restore rowY in onSeriesBackgroundMouseMove.
I'd suggest to do that in the place where you dispose/restore the seriesInstances but still it will not work with legend box.
To fix it you can just dispose RowsY and RowX instead of resultTable and restore it after check:
rowX.restore()
series.forEach((el, i)=>{
//check if series was disposed
if(!el.isDisposed()){
rowsY[i].restore()
}
})
Also, in v.4.0 we will add new API that replaces dispose/restore
Here is updated code from the example that you use
// Import LightningChartJS
const lcjs = require("#arction/lcjs");
// Import data-generators from 'xydata'-library.
const { createProgressiveTraceGenerator } = require("#arction/xydata");
// Extract required parts from LightningChartJS.
const {
lightningChart,
AutoCursorModes,
UIElementBuilders,
UILayoutBuilders,
UIOrigins,
translatePoint,
Themes,
} = lcjs;
// Create a XY Chart.
const chart = lightningChart()
.ChartXY({
theme: Themes.lightNew,
})
// Disable native AutoCursor to create custom
.setAutoCursorMode(AutoCursorModes.disabled)
.setTitle('Custom Cursor using LCJS UI')
// set title for Y axis
chart.getDefaultAxisY().setTitle('Y-axis')
// generate data and creating the series
const series = new Array(3).fill(0).map((_, iSeries) => {
const nSeries = chart.addLineSeries({
dataPattern: {
// pattern: 'ProgressiveX' => Each consecutive data point has increased X coordinate.
pattern: 'ProgressiveX',
},
})
createProgressiveTraceGenerator()
.setNumberOfPoints(200)
.generate()
.toPromise()
.then((data) => {
return nSeries.add(data)
})
return nSeries
})
// Add Legend.
const legend = chart.addLegendBox().add(chart)
// Create UI elements for custom cursor.
const resultTable = chart
.addUIElement(UILayoutBuilders.Column, {
x: chart.getDefaultAxisX(),
y: chart.getDefaultAxisY(),
})
.setMouseInteractions(false)
.setOrigin(UIOrigins.LeftBottom)
.setMargin(5)
.setBackground((background) =>
background
// Style same as Theme result table.
.setFillStyle(chart.getTheme().resultTableFillStyle)
.setStrokeStyle(chart.getTheme().resultTableStrokeStyle),
)
const rowX = resultTable.addElement(UILayoutBuilders.Row).addElement(UIElementBuilders.TextBox)
const rowsY = series.map((el, i) => {
return resultTable
.addElement(UILayoutBuilders.Row)
.addElement(UIElementBuilders.TextBox)
.setTextFillStyle(series[i].getStrokeStyle().getFillStyle())
})
const tickX = chart.getDefaultAxisX().addCustomTick().setAllocatesAxisSpace(false)
const ticksY = series.map((el, i) => {
return chart
.getDefaultAxisY()
.addCustomTick()
.setAllocatesAxisSpace(false)
.setMarker((marker) => marker.setTextFillStyle(series[i].getStrokeStyle().getFillStyle()))
})
// Hide custom cursor components initially.
// resultTable.dispose()
rowsY.forEach(el=>{
el.dispose()
})
tickX.dispose()
ticksY.forEach((tick) => tick.dispose())
// Implement custom cursor logic with events.
chart.onSeriesBackgroundMouseMove((_, event) => {
const mouseLocationClient = { x: event.clientX, y: event.clientY }
// Translate mouse location to LCJS coordinate system for solving data points from series, and translating to Axes.
const mouseLocationEngine = chart.engine.clientLocation2Engine(mouseLocationClient.x, mouseLocationClient.y)
// Translate mouse location to Axis.
const mouseLocationAxis = translatePoint(mouseLocationEngine, chart.engine.scale, series[0].scale)
// Solve nearest data point to the mouse on each series.
const nearestDataPoints = series.map((el) => el.solveNearestFromScreen(mouseLocationEngine))
// Find the nearest solved data point to the mouse.
const nearestPoint = nearestDataPoints.reduce((prev, curr, i) => {
if (!prev) return curr
if (!curr) return prev
return Math.abs(mouseLocationAxis.y - curr.location.y) < Math.abs(mouseLocationAxis.y - prev.location.y) ? curr : prev
})
if (nearestPoint) {
// Set custom cursor location.
resultTable.setPosition({
x: nearestPoint.location.x,
y: nearestPoint.location.y,
})
// Change origin of result table based on cursor location.
if (nearestPoint.location.x > chart.getDefaultAxisX().getInterval().end / 1.5) {
if (nearestPoint.location.y > chart.getDefaultAxisY().getInterval().end / 1.5) {
resultTable.setOrigin(UIOrigins.RightTop)
} else {
resultTable.setOrigin(UIOrigins.RightBottom)
}
} else if (nearestPoint.location.y > chart.getDefaultAxisY().getInterval().end / 1.5) {
resultTable.setOrigin(UIOrigins.LeftTop)
} else {
resultTable.setOrigin(UIOrigins.LeftBottom)
}
// Format result table text.
rowX.setText(`X: ${chart.getDefaultAxisX().formatValue(nearestPoint.location.x)}`)
rowsY.forEach((rowY, i) => {
rowY.setText(`Y${i}: ${chart.getDefaultAxisY().formatValue(nearestDataPoints[i]?.location.y || 0)}`)
})
// Position custom ticks.
tickX.setValue(nearestPoint.location.x)
ticksY.forEach((tick, i) => {
tick.setValue(nearestDataPoints[i]?.location.y || 0)
})
// Display cursor.
rowX.restore()
series.forEach((el, i)=>{
if(!el.isDisposed()){
rowsY[i].restore()
}
})
tickX.restore()
ticksY.map((el) => el.restore())
} else {
// Hide cursor.
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
}
})
chart.onSeriesBackgroundMouseLeave((_, e) => {
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
})
chart.onSeriesBackgroundMouseDragStart((_, e) => {
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
})
function disposeCustomCursor() {
rowX.dispose()
rowsY.forEach(el=>{
el.dispose()
})
}
setTimeout(() => {
series[0].dispose()
rowsY[0].dispose()
}, 3000);
setTimeout(() => {
series[0].restore()
rowsY[0].restore()
}, 6000);
I'm creating an extension that is a sticky button on the browser and when clicked it loads the next article you've saved. These articles are stored in a firebase DB and are being fetched on page load.
I've added a pointer variable to index the array and storing the value of the pointer in local storage so I have it as the pages refresh. I'm able to subtract the value of the pointer correctly and when I try to load the next URL upon click for some reason it loads an entirely different URL.
the shape of fetched data:
data = [
{
book: "the matrix,
url: 'https://thisurl1.com
},
{
book: "the matrix 2,
url: 'https://thisurl2.com
},
{
book: "the matrix 3,
url: 'https://thisurl3.com
}
]
here's the code:
// check if local storage is available
const storageAvailable = (type) => {
}
// fetches articles from article endpoint => [data]
const fetchArticles = async () => {
try {
const response = await fetch("url_endpoint");
const data = await response.json();
articleStorage = Object.values(data);
localStorage.setItem("articles", JSON.stringify(articleStorage))
const pointer = Number(localStorage.getItem("pointer"));
if (pointer === null || pointer < 0 || pointer > articleStorage.length - 1) {
localStorage.setItem("pointer", articleStorage.length - 1);
}
return;
} catch (err) {
console.log(err);
}
};
// create the next button
const nextButton = () => {
// creating tags and buttons
// styling the button
// loads next article on click
button.addEventListener("click", loadNextArticle);
// appending to dom
};
// loads next article in array
const loadNextArticle = () => {
const pointer = Number(localStorage.getItem("pointer"));
const newPointer = pointer - 1;
const articleStorage = JSON.parse(localStorage.getItem('articles'));
if (pointer < 0 || pointer > articleStorage.length - 1) {
alert('nothing else to show');
} else {
localStorage.setItem('pointer', newPointer);
window.location.href = articleStorage[newPointer].link;
}
};
window.onload = () => {
if (storageAvailable('localStorage')) {
if (localStorage.getItem("articles") === null) fetchArticles();
nextButton();
} else {
console.log('local storage not available');
}
};
You never update pointer
console.log(pointer); // <-- read pointer
localStorage.setItem("pointer", pointer - 1); // <-- update local storage
if (pointer < 0 || pointer > articleStorage.length - 1) { // <-- still the same pointer value
You need to update the variable since it will not update on its own
pointer--; // update the variable
localStorage.setItem("pointer", pointer);
if (pointer < 0 || pointer > articleStorage.length - 1) {
the parent component makes a call to the api and pushes data to the sharedData service and subscribes to it. So everytime a change has been made to the date set its automatically updated in the parent component.
ngOnInit() {
this.initialise();
}
initialise() {
this.dataShareService.setDataFilter(null);
this.loading = true;
this.assetService.table_data().subscribe(res => {
this.dataShareService.setEnviromentData(res);
this.dataShareService.enviromeData.subscribe(r => {
this.loading = false;
this.data = r;
let i = 0;
console.log('just a ocunt', i++)
this.searchValue = r['results'].map((val: any) => {
return val.name;
});
this.totalItems = this.data['results'].length;
this.loading = false;
}, err => {
console.log(err);
});
}
this data is then passed down to the child component - which is the data table
<data-table
*ngIf="!loading"
class="flex_display_centre"
[showTable]="showTable"
[columns]="columns"
[limit]="limit"
[data]="data">
</data-table>
I am the onScroll function to to get the why coordinates.
<ngx-datatable
[scrollbarV]="true"
[loadingIndicator]="isLoading"
[limit]="limit"
(scroll)='onScroll($event.offsetY)'>
</ngx-datatable>
onScroll(offsetY: number) {
this.isLoading = true;
let numberViewRows = 12;
const viewHeight = this.el.nativeElement.getBoundingClientRect().height - this.headerHeight;
if (!this.isLoading && offsetY >= this.rowHeight * numberViewRows ) {
this.loadPageData.emit(this.data['next']);
this.isLoading = false;
}
}
Once the condition is true an emitter is sent to the parent component to get the next page/data
nextPage(pageNum: any) {
this.isLoading = true;
this.assetService.reloadDataTable(pageNum).subscribe(results => {
this.dataShareService.setEnviromentData(results);
this.isLoading = false;
}, err => {
console.log('err', err)
});
}
data share service:
this first time the service get data it assigns the to previousEnviromentData variable. everytime the api is called the next/prev and count is updated and the results array is appended to the previous data
private enviromeInfoData = new BehaviorSubject([{}]);
enviromeData = this.enviromeInfoData.asObservable();
private previousEnviromentData;
setEnviromentData(data: any) {
if (data['results'] !== this.previousEnviromentData && this.previousEnviromentData !== undefined) {
this.previousEnviromentData["count"] = data.count,
this.previousEnviromentData["next"] = data.next,
this.previousEnviromentData["page_count"] = data.page_count,
this.previousEnviromentData["previous"] = data.previous,
this.previousEnviromentData['results'].push(...data['results']);
this.enviromeInfoData.next(this.previousEnviromentData);
} else {
this.previousEnviromentData = data;
this.enviromeInfoData.next(data);
}
}
However when I scroll to the bottom of the table the data is updated in the parent components and data share servuce but never updates in the table.
What am I doing wrong/missing?
Following code example provided by Telerik I am trying to preserve Kendo Grid state. Everything seems to be working but whenever I use date column filter and reload page the date in filter is decreased by one day... I pick 5th Oct., refresh and BAM! filter says 4th Oct. :) Any clues?
$(document).ready(function () {
var grid = $("#ProjectsGrid").data("kendoGrid");
var state = JSON.parse($.cookie("projectsState"));
if (state) {
if (state.filter) {
parseFilterDates(state.filter, grid.dataSource.options.schema.model.fields);
}
grid.dataSource.query(state);
}
else {
grid.dataSource.read();
}
});
function dataBound(e) {
var grid = this;
var dataSource = this.dataSource;
var state = kendo.stringify({
page: dataSource.page(),
pageSize: dataSource.pageSize(),
sort: dataSource.sort(),
group: dataSource.group(),
filter: dataSource.filter()
});
$.cookie("projectsState", state);
if ($.cookie('empRows')) {
$.each(JSON.parse($.cookie('empRows')), function() {
var item = dataSource.get(this);
var row = grid.tbody.find('[data-uid=' + item.uid + ']');
row.addClass('k-state-selected');
})
}
}
function parseFilterDates(filter, fields) {
if (filter.filters) {
for (var i = 0; i < filter.filters.length; i++) {
parseFilterDates(filter.filters[i], fields);
}
}
else {
if (fields[filter.field].type == "date") {
filter.value = kendo.parseDate(filter.value);
}
}
}
Occassionally I get the exception "PageMap asked for range which it does not have" from my Ext Js 4.2.1 infinite scrolling grid. It is raised in data/PageMap.js on line 211. Of course one should not ask for non-existing entries, but this is sometimes done by the framework itself. Seems to be somehow connected to adding/removing records or reloading the grid. There are already some threads on this topic in the Sencha forum, e.g. this, but no killer solution or bugfix was proposed yet.
Meanwhile, I have to keep this exception from the users' eyes. What would be a good way to do so? Tricky thing is that it is sometimes provoked just by the user moving the scrollbar, so there is no single line of my code directly involved.
I found the root cause to be that when it's rendering rows, it determines if it's before a selected row. If it's working on the last row, it still looks for row + 1. (Ext.view.Table:931 in 4.2.1)
My simple solution is to just make it return false:
Ext.override(Ext.selection.RowModel,
{
isRowSelected: function (record, index)
{
try
{
return this.isSelected(record);
}
catch (e)
{
return false;
}
}
});
Christoph,
I have similar troubles with "PageMap asked for range which it does not have" during asynchronuous refreshing of grids. I catched some of sources of errors in the ExtJS 4.2.1 code and created simple override, that works for me. You can try if it will work for you. I will be happy for your feedback.
Ext.override(Ext.view.Table, {
getRecord: function (node) {
node = this.getNode(node);
if (node) {
var recordIndex = node.getAttribute('data-recordIndex');
if (recordIndex) {
recordIndex = parseInt(recordIndex, 10);
if (recordIndex > -1) {
// Eliminates one of sources of "PageMap asked for range which it does not have" error
if (this.store.getCount() > 0) {
return this.store.data.getAt(recordIndex);
}
}
}
return this.dataSource.data.get(node.getAttribute('data-recordId'));
}
},
renderRow: function (record, rowIdx, out) {
var me = this,
isMetadataRecord = rowIdx === -1,
selModel = me.selModel,
rowValues = me.rowValues,
itemClasses = rowValues.itemClasses,
rowClasses = rowValues.rowClasses,
cls,
rowTpl = me.rowTpl;
rowValues.record = record;
rowValues.recordId = record.internalId;
rowValues.recordIndex = rowIdx;
rowValues.rowId = me.getRowId(record);
rowValues.itemCls = rowValues.rowCls = '';
if (!rowValues.columns) {
rowValues.columns = me.ownerCt.columnManager.getColumns();
}
itemClasses.length = rowClasses.length = 0;
if (!isMetadataRecord) {
itemClasses[0] = Ext.baseCSSPrefix + "grid-row";
if (selModel && selModel.isRowSelected) {
var storeRows = this.getStore().getCount();
// Eliminates one of sources of "PageMap asked for range which it does not have" error
if (rowIdx + 1 < storeRows) {
if (selModel.isRowSelected(rowIdx + 1)) {
itemClasses.push(me.beforeSelectedItemCls);
}
}
if (selModel.isRowSelected(record)) {
itemClasses.push(me.selectedItemCls);
}
}
if (me.stripeRows && rowIdx % 2 !== 0) {
rowClasses.push(me.altRowCls);
}
if (me.getRowClass) {
cls = me.getRowClass(record, rowIdx, null, me.dataSource);
if (cls) {
rowClasses.push(cls);
}
}
}
if (out) {
rowTpl.applyOut(rowValues, out);
} else {
return rowTpl.apply(rowValues);
}
}
});
all these codes don't work for me, after many debugging I wrote this override which solve the problem.
Ext.define('overrides.LruCache', {
override: 'Ext.util.LruCache',
// private. Only used by internal methods.
unlinkEntry: function (entry) {
// Stitch the list back up.
if (entry) {
if (this.last && this.last.key == entry.key)
this.last = entry.prev;
if (this.first && this.first.key == entry.key)
this.first = entry.next;
if (entry.next) {
entry.next.prev = entry.prev;
} else {
this.last = entry.prev;
}
if (entry.prev) {
entry.prev.next = entry.next;
} else {
this.first = entry.next;
}
entry.prev = entry.next = null;
}
}
});
This is my solution for my specific case with the same error
it somehow lost DOM element for child
this code fix that
Ext.define('override.Ext.view.Table', {
/**
* Returns the node given the passed Record, or index or node.
* #param {HTMLElement/String/Number/Ext.data.Model} nodeInfo The node or record
* #param {Boolean} [dataRow] `true` to return the data row (not the top level row if wrapped), `false`
* to return the top level row.
* #return {HTMLElement} The node or null if it wasn't found
*/
override: 'Ext.view.Table',
getNode: function (nodeInfo, dataRow) {
// if (!dataRow) dataRow = false
var fly,
result = this.callParent(arguments)
if (result && result.tagName) {
if (dataRow) {
if (!(fly = Ext.fly(result)).is(this.dataRowSelector)) {
result = fly.down(this.dataRowSelector, true)
}
} else if (dataRow === false) {
if (!(fly = Ext.fly(result)).is(this.itemSelector)) {
result = fly.up(this.itemSelector, null, true)
}
if (this.xtype == 'gridview' && !this.body.dom.querySelector(`#${result.id}`)) {
result = null
}
}
}
return result
},
})