Plotting timestamp on the x-axis based on the start date - javascript

My Goal:
I'm trying to make my timeline (with horizontal rectangles) timestamp-dependent and wondering how to move forward. In the future, the user of the application will have an option to select a start date. Let's say if start date is 04/01/2021 12:00 am, then I want the Text Timeline to be divided into 30 days or less based on how it looks on the UI. This functionality will enable me to put a dot if I want to, on the timeline based on the timestamp. For example, the data that I've, as mentioned inside InsightTimeLine.tsx, if I want to put a dot at "Timestamp": "04/06/2021 18:15:00", for Text 4, I might be able to do that on the horizontal line. With the current setup, I'm not able to do this.
Brief Description about Files:
File 1: App.tsx: This is where a Timeline component is called.
File 2: InsightTimeline.tsx:This is where I've my data defined and I'm making the timeline using makeTimelinetrace function
File 3: Plot.tsx :This is where the plotting of timeline is done based on some calculations.
I'm pasting my code below and also a CodeSandBox link for the existing setup:
App.tsx
export default function App() {
return (
<div>
<div className="pin">
<h2 style={{ display: "inline-block" }}>Text Timeline </h2>
<Timeline />
</div>
</div>
);
}
InsightTimeline.tsx:
function InsightTimeline() {
const iCanvasContainer = useRef(null);
const plot = d3.select(iCanvasContainer.current);
const [bins, setBins] = useState(14);
const [timeline, setTimeline] = useState(new Plot(plot, bins));
useEffect(() => {
if (bins) {
setTimeline(new Plot(plot, bins));
}
}, [bins]);
useEffect(() => {
if (iCanvasContainer.current) {
timeline.refreshTimeline();
var i = 0;
var data = [
{
ID: "3",
Object: "Text 1",
Timestamp: "05/12/2020 13:26:00"
},
{
ID: "6",
Object: "Text 2",
Timestamp: "01/07/2020 18:59:00"
},
{
ID: "7",
Object: "Text 3",
Timestamp: "01/07/2020 18:49:00"
},
{
ID: "57",
Object: "Text 4",
Timestamp: "04/06/2021 18:15:00"
}
];
if (data) {
data?.map((datatext: any) => {
i += 1;
timeline.makeTimelineTrace(i, datatext.Object.toUpperCase());
});
}
timeline.doRefresh();
}
}, [timeline]);
return <svg ref={iCanvasContainer} />;
}
export default InsightTimeline;
Plot.tsx
class Plot {
logname: string = "Plotter";
plot: any;
legendWidth: number = 50;
timelineBins: number = 14;
timelineSpace: number;
timelineRow: number = 22;
timelineThickness: number = 10;
timelineMarginTop: number = 25;
timelineDelta: any;
layer_base: any;
layer_text: any;
constructor(public inPlot: any, public inBins?: number) {
if (inBins) this.timelineBins = inBins;
this.timelineSpace = (1000 - this.legendWidth) / (this.timelineBins + 1);
try {
console.log(`${this.logname}: D3 Init: Creating Plot Container.`);
this.plot = inPlot;
this.plot.attr("class", "plot");
this.layer_base = this.plot.append("g").attr("class", "base_layer");
this.layer_text = this.plot.append("g").attr("class", "base_layer");
console.log(`${this.logname}: D3 Init Done.`);
} catch (error) {
console.log(`${this.logname}: ERROR - Could not create plot. (${error})`);
if (!this.plot) console.log(`${this.logname}: Reference Not Defined.`);
if (!this.timelineRow)
console.log(`${this.logname}: Timeline Row Not Defined.`);
}
}
getLogName() {
return this.logname;
}
doRefresh() {
console.log(`${this.logname}: ** REFRESH`);
this.plot.exit().remove();
}
destroy() {
this.plot = undefined;
}
makeTimelineTrace(row: number, label: string) {
this.layer_base
.append("rect")
.attr("class", "timeline_trace")
.attr("x", 0)
.attr("y", this.timelineRow * row + this.timelineThickness / 2);
this.layer_text
.append("text")
.attr("class", "timeline_text")
.attr("x", 15)
.attr("y", this.timelineRow * row + (this.timelineThickness - 5) / 2)
.classed("label", true)
.text(label);
}
refreshTimeline() {
// this.plot.selectAll("text").remove();
// this.plot.selectAll("rect").remove();
}
}
export default Plot;
The code sandbox browser should show the following on your end:
P.S. If for some reason you don't see anything below the Text Timeline text after opening the codesandbox, you can go to any file, for example, Plot,tsx hit space and once it gets saved it will show up in the browser. Not sure why it is happening in sandbox though but this is the hack I found.

Related

Highchart Legend with Checkbox, events, hover style, symbol order change

I currently have Highcharts implemented in a Chart component in my application, but I need to make some changes to the Legend, went through most of the documentation, created some functions with Highcharts.wrap().
First, the Legend was simple, each legend item being
[Symbol] [Label] .
But now I need to change it into:
[Checkbox] [Label] [Symbol]
Here is what I got so far:
[Checkbox] [Symbol] [Label]
And with the click on the checkbox replicating the click on the Legend (symbol, label), which shows/hide the series line.
how? with this: (showing only the important parts)
const defaultOptions: Highcharts.Options = {
...,
legend: {
borderColor: "transparent",
verticalAlign: "top",
align: "left",
x: 14,
itemCheckboxStyle: {
cursor: "pointer",
border: "1px solid #62737a",
},
},
...,
plotOptions: {
series: {
...,
showCheckbox: true,
selected: true,
events: {
checkboxClick: function () {
this.setVisible(!this.visible);
},
},
},
...,
},
...,
}
If we only use showCheckbox: true, the checkbox will be far on the right side of each label, not ideal. So this is needed: (If possible I also would like tips on how to avoid the any error on TS in this case, without the comments).
Highcharts.wrap(Highcharts.Legend.prototype, "positionCheckboxes", legendCheckboxPosition);
function legendCheckboxPosition(
// eslint-disable-next-line #typescript-eslint/no-explicit-any
this: any,
// eslint-disable-next-line #typescript-eslint/no-explicit-any
p: any,
scrollOffset: number
) {
const alignAttr = this.group.alignAttr;
const clipHeight = this.clipHeight || this.legendHeight;
let translateY: number;
if (alignAttr) {
translateY = alignAttr.translateY;
Highcharts.each(
this.allItems,
function (item: {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
checkbox: any;
// eslint-disable-next-line #typescript-eslint/no-explicit-any
legendItem: { getBBox: (arg0: boolean) => any };
// eslint-disable-next-line #typescript-eslint/no-explicit-any
checkboxOffset: any;
}) {
const checkbox = item.checkbox;
const bBox = item.legendItem.getBBox(true);
let top;
if (checkbox) {
top = translateY + checkbox.y + (scrollOffset || 0) + 2;
Highcharts.css(checkbox, {
left:
alignAttr.translateX +
item.checkboxOffset +
checkbox.x -
100 -
bBox.width +
17 +
"px",
top: top + "px",
display: top > translateY - 6 && top < translateY + clipHeight - 6 ? "" : "none",
});
}
}
);
}
}
But with this done, I still need to make some changes, which are:
Change the order of Symbol and Label
There is supposed to be a rtl property inside the legends options, which is supposed to change the order of Symbol and Label , but if I do that, it reverses, but it also reverse the order of the legends somehow, I'll show:
-> Without rtl:
-> With rtl: true inside the legends options:
The checkbox distance I understand, because it will need to change my legendCheckboxPosition function, my real problem here is the order of the legends being changed, like if I used legend.reversed: true.. I found out that I can use the reversed property to fix this, but I was wondering if this was a bug with something else..because in the documentation the rtl property only changes the order of Symbol and Label, not the legends order.
This is what I need:
I need to put a style in the :hover of the checkbox, I tried using the legend.itemCheckboxStyle but that doesn't allow me to add hover effects... (I need to place a box-shadow when hovering the checkbox)
ONE ISSUE SOLVED: Another issue is when clicking the legend item (which is separated of the checkbox)
When clicking the legend item, it shows/hide the series, but it doesn't change the checkbox selection.
I know that the checkbox selection is determined by the series.selected property, and that I have the legendItemClick event inside the plotOptions.series.events, but inside that I don't have a this.setSelected function, only this.setVisible function. I tried using that, but it seems to freeze the chart, not doing anything.
How to change the checkbox selection when clicking only in the legend item?
Edit: Managed to solve this by adding this event to options.plotOptions.series.events :
legendItemClick: function () {
const seriesIndex = this.index;
this.chart.series[seriesIndex].select();
},
Well.. that is my problem, with the hope that you guys can help me solve it.
A possible way to arranging the elements of legend would be to edit the legend in legend.labelFormatter and add a Unicode line character. To have the checkbox on the right also you can style it in legend.itemCheckboxStyle.
legend: {
symbolWidth: 0,
useHTML: true,
labelFormatter: function() {
return `<div>${this.name}<span style="color: green;">━</span></div>`;
},
itemDistance: 50,
itemCheckboxStyle: {
"width": "13px",
"height": "13px",
"margin-left": "-130px",
"position": "absolute"
}
},
API References:
https://api.highcharts.com/highcharts/legend.itemCheckboxStyle
Demo: https://jsfiddle.net/BlackLabel/sbcL7rp9/3/
After a lot of research and experimenting, I managed to have it all working.
Options (default options and events, also toggling visibility on load)
const series = GetSeries(opts, null);
...
// Check if Series is supposed to be hidden from load (from the checkbox selected prop), if it is, hide it
series.forEach((serie) => {
if (serie.selected !== undefined && serie.selected === false) serie.visible = false;
});
const defaultOptions: Highcharts.Options = {
...,
legend: {
borderColor: "transparent",
verticalAlign: "top",
align: "left",
x: 14,
},
...,
plotOptions: {
series: {
...
showCheckbox: true,
selected: true,
events: {
checkboxClick: function () {
this.setVisible(!this.visible);
},
legendItemClick: function () {
const seriesIndex = this.index;
this.chart.series[seriesIndex].select();
},
},
},
...
},
...
};
return mergeObjects(defaultOptions, opts);
}
And a lot of resolved extending Highcharts:
// Adjust position of the Highchart Legend Checkbox, and switch label and symbol in the legend
function legendPositionAdjustments(
// eslint-disable-next-line #typescript-eslint/no-explicit-any
this: any,
// eslint-disable-next-line #typescript-eslint/no-explicit-any
p: any,
scrollOffset: number
) {
const pixelsToREM = (value: number) => {
return value / 16;
};
const alignAttr = this.group.alignAttr;
const clipHeight = this.clipHeight || this.legendHeight;
let translateY: number;
const legendMainElement = this.box.parentGroup.element;
// Adjust the main legend element to be closer to the checkbox
if (legendMainElement) {
Highcharts.attr(legendMainElement, "transform", "translate(19, 10)");
}
if (alignAttr) {
translateY = alignAttr.translateY;
this.allItems.forEach(function (item: {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
checkbox: any;
// eslint-disable-next-line #typescript-eslint/no-explicit-any
legendItem: { getBBox: (arg0: boolean) => any };
// eslint-disable-next-line #typescript-eslint/no-explicit-any
checkboxOffset: any;
// eslint-disable-next-line #typescript-eslint/no-explicit-any
legendGroup: any;
}) {
const bBox = item.legendItem.getBBox(true);
// Change position of Label and Symbol in the Highcharts Legend
const legendItemElement = item.legendGroup.element;
const legendItemPath = legendItemElement.querySelector("path.highcharts-graph");
const legendItemPoint = legendItemElement.querySelector("path.highcharts-point");
const legendItemText = legendItemElement.querySelector("text");
if (legendItemPath) {
Highcharts.attr(legendItemPath, "transform", `translate(${bBox.width + 3}, 0)`);
}
if (legendItemPoint) {
Highcharts.attr(legendItemPoint, "transform", `translate(${bBox.width + 3}, 0)`);
}
if (legendItemText) {
Highcharts.attr(legendItemText, "x", 0);
}
// Adjust the position of the checkbox to the left side of the Highcharts Legend
const checkbox = item.checkbox;
let top;
let left;
if (checkbox) {
top = translateY + checkbox.y + (scrollOffset || 0) + 4;
left = alignAttr.translateX + item.checkboxOffset + checkbox.x - 100 - bBox.width + 17;
Highcharts.css(checkbox, {
left: pixelsToREM(left) + "rem",
top: pixelsToREM(top) + "rem",
display: top > translateY - 6 && top < translateY + clipHeight - 6 ? "" : "none",
});
}
});
}
}
// This function is called when triggering show/hide of series, always calling with visibility = true
// eslint-disable-next-line #typescript-eslint/no-explicit-any, #typescript-eslint/no-unused-vars
function colorizeLegendRegardlessOfVisibility(this: any, proceed: any, item: any, visible: any) {
proceed.call(this, item, true);
}
This will:
Avoid changing the color of the legend and symbol when series is hidden or legend is out of focus
Correct position the checkbox to left side of the legend
Switch positions of the symbol and label, which are in the SVG
Hope to help someone who encounters similar problems in the future.

React-Plotly: Graph Not Displaying Data From Array

I'm using react-plotly for implementing a line graph inside a React functional component as shown in the code below. However, when I try to plot my data using an array of values, it won't show anything. I'm using scattergl because I have a big set of data points to display. I need help with this issue. I'm using react.js version 17.0.2 and react-plotly version 2.5.1.
Here's my code:
import React from 'react';
import Plot from 'react-plotly.js';
import mydata from '../data/allData.json';
const MyApp = () => {
// Transform Data
const transformData = mydata.map((d) => {
let traces = [];
let plot_data = [];
let xx = [];
let yy = [];
let ss = [];
ss.push(d.mydata_id)
d.data.map((each) => {
xx.push(each.datetime_read)
yy.push(each.surge_count)
});
plot_data['x'] = xx;
plot_data['y'] = yy;
plot_data['s'] = ss;
// console.log("Plot Data: ", plot_data)
traces.push({
x: plot_data['x'],
y: plot_data['y'],
type: 'scattergl',
marker: {color: 'green'},
name: plot_data['s']
});
return traces
});
return (
<div>
<div>
<Plot
data={transformData}
layout={{...}}
/>
</div>
</div>
)
};
Here's my data:
[
{
"mydata_id": "00175784",
...
"data": [
{
"datetime_read": "2022-03-16 03:15:00",
...
"surge_count": "240"
},
{
...
}
]
},
{
"mydata_id": "00161784",
...
"data": [
{
"datetime_read": "2022-03-16 03:20:00",
...
"surge_count": "194"
},
{
"datetime_read": "2022-03-16 03:15:00",
...
"surge_count": "342"
},
{
...
}
]
}
]
I would also like to note that when I try to display specific data in the array with data={transformData[2]}, it works just fine. But my goal is to display all data in the transformData array.

Angular component need to be refresh after add data

Very difficult to resolve that for me. I've a component who get Json data from 2 different url to create a chart:
1 update data is for the old values
the other for the real time value
Here is the component:
#Component({
selector: 'app-staticchart',
templateUrl: './staticchart.component.html',
styleUrls: ['./staticchart.component.scss']
})
export class StaticchartComponent implements OnInit {
candles: Observable<Candlecollect[]>;
data: Array<any>;
candlecollect: Candlecollect;
start = Date.now();
end = Date.now() + 1;
constructor(private candlecollectService: CandlecollectService,
public spinnerService: SpinnerService) {
}
ngOnInit(): void {
this.loadChart();
}
loadChart(): void {
/*
* Request to get the 500 latest Candlesticks - send by rest to the back api
*/
const answer = this.candlecollectService.extractCandles('url_1')
.subscribe(data => {
// tslint:disable-next-line:forin
for (let counter = 1; counter < 500; counter++) {
candleSeries.update({
// #ts-ignore
time: data.openTime / 1000,
open: data.open,
high: data.high,
low: data.low,
close: data.close
});
}
});
/*
* Request send by REST to get the real time values
*/
const binanceSocket = new WebSocket('url_2');
binanceSocket.onmessage = function(event) {
const message = JSON.parse(event.data);
// console.log(event.data);
const candlestick = message.k;
candleSeries.update({
// #ts-ignore8
time: candlestick.t / 1000,
open: candlestick.o,
high: candlestick.h,
low: candlestick.l,
close: candlestick.c
});
};
}
}
Here is the html:
<mat-card class="dashboard-card">
<div id="chart" fxFill></div>
</mat-card>
It works perfectly but I have this problem: I need to refresh the page or the chart doesn't appear.
So I've tried to put a SpinnerService to wait the loading but it doesn't works.
I'm sorry if this question seems stupid but I work with angular for 1 month and it's little bit complicated...^^

Updating in chart.js

I'm trying to figure out how to update a chart.js chart. Google's returned with a lot of answers and I think some are outdated because I can't seem to get any of the solutions to work. The documentation page says just use chartname.update() but it doesn't seem to work for me. I already checked console to make sure the chart object was updating. For some reason the chart itself on the page just isn't changing.
let chartContainer = document.getElementById('charts');
let overview = {
create: function () {
let chartCanvas = document.createElement('canvas');
chartCanvas.id = 'overviewChart';
chartCanvas.appendChild(document.createTextNode('test'));
chartContainer.appendChild(chartCanvas);
let overviewChart = document.getElementById('overviewChart').getContext('2d');
renderChart = new Chart(overviewChart, {
type: 'bar',
data: {
labels:subjectList,
datasets: [{
barThickness: 'flex',
label: 'Completed Credits',
data: []
}]
},
options: {
}
})
},
reload: function() {
console.log('reloaded overview chart');
renderChart.data.datasets.data = [];
for (subject in classes) {
console.log('adding: ' + classes[subject].count)
renderChart.data.datasets.data.push(classes[subject].count);
}
renderChart.update();
}
}
function reloadCharts() {
overview.reload();
}
overview.create();
There are problems in your reload function where you access renderChart.data.datasets.
Please note that renderChart.data.datasets is an array. Therefore, you need to make the following changes:
reload: function() {
// renderChart.data.datasets.data = []; // old
renderChart.data.datasets[0].data = []; // new
for (subject in classes) {
console.log('adding: ' + classes[subject].count)
// renderChart.data.datasets.data.push(classes[subject].count); // old
renderChart.data.datasets[0].data.push(classes[subject].count); // new
}
renderChart.update();
}

Angular Prime NG - How to add a query string in the router link for the Prime NG steps

I have created steps using angular and prime NG , I would like to add query string within the link of the steps.
I am creating the steps as below -
<p-steps [model]="steps" [readonly]="false" [(activeIndex)]="activeIndex"></p-steps>
for (let i = 0; i < this.steps.length; i++) {
this.steps.push(
{
label: this.steps[i].name,
routerLink: ["step"],
command: (event: any) => {
this.activeIndex = i + 1;
this.clickStep(this.stepss[i]);
}
}
);
}
Here I would like to add query string with the link. Please suggest for any way out for the same?
PrimeNG reference -https://primefaces.org/primeng/#/steps
Since Angular's change detection mechanism looks for a reference change, I would recommend using the map function since it returns a new array reference.
this.steps = this.steps.map(step => {
return {
label: step.name,
routerLink: ["step?a=1&b=2"],
command: (event: any) => {
this.activeIndex = i + 1;
this.clickStep(step);
}
}
})
Using the router link as the following might to the trick
routerLink: ["step?a=1&b=2"]
I am able to add the query string with prime NG steps link using the below code -
queryParams: { "name": this.plugins[i].name }
complete code would be like below-
this.steps = this.steps.map(step => {
return {
label: step.name,
routerLink: ["step?a=1&b=2"],
queryParams: { "name": this.plugins[i].name }
command: (event: any) => {
this.activeIndex = i + 1;
this.clickStep(step);
}
}
});

Categories