-
Notifications
You must be signed in to change notification settings - Fork 33
Added Configurable Time Stamp Formatting in Chart Panel #481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b5fb87f
9ead4b0
1a326a6
4509d7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ export function quickVis( | |
| let cachedResponse: any | ||
| let cachedQuery: AnyIfEmpty | ||
| let requestActive: boolean = false | ||
| let selectedTimeFormat: string = "auto" | ||
|
|
||
| const chartTypePicker = new SlimSelect({ | ||
| select: "#_qvis_frm_chart_type", | ||
|
|
@@ -94,6 +95,10 @@ export function quickVis( | |
| select: "#_qvis_frm_axis_y", | ||
| }) | ||
|
|
||
| const timeFormatPicker = new SlimSelect({ | ||
| select: "#_qvis_frm_time_format", | ||
| }) | ||
|
|
||
| function resize() { | ||
| echart.resize() | ||
| } | ||
|
|
@@ -104,6 +109,93 @@ export function quickVis( | |
| } | ||
| } | ||
|
|
||
| function detectTimeColumns(columns: any[]) { | ||
| return columns.filter(col => { | ||
| // Detect timestamp columns by type or name patterns | ||
| return col.type === 'TIMESTAMP' || | ||
| col.type === 'TIMESTAMP_NS' || | ||
| /timestamp|time|date|created|updated/i.test(col.name) | ||
| }) | ||
| } | ||
|
|
||
| function isValidTimestamp(value: any): boolean { | ||
| if (typeof value === 'string') { | ||
| const parsed = Date.parse(value) | ||
| return !isNaN(parsed) && parsed > 0 | ||
| } | ||
| if (typeof value === 'number') { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In which cases does QuestDB return a number for a timestamp column? Why do we have this check?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the time is stored in milliseconds since epoch for example. |
||
| return value > 0 && value < 9999999999999 | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| function formatTimestamp(timestamp: any, format: string): string { | ||
| let date: Date | ||
|
|
||
| if (typeof timestamp === 'string') { | ||
| date = new Date(timestamp) | ||
| } else if (typeof timestamp === 'number') { | ||
| date = new Date(timestamp) | ||
| } else { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which case is covered in this branch? Can you give an example?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are asking about number, then like before it could be the milliseconds type. Or an ISO Format String. |
||
| return timestamp.toString() | ||
| } | ||
|
|
||
| if (isNaN(date.getTime())) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which case is covered in this branch? Can you give an example? |
||
| return timestamp.toString() | ||
| } | ||
|
|
||
| // Helper function to pad numbers with leading zeros | ||
| const pad = (num: number): string => num.toString().padStart(2, '0') | ||
|
|
||
| switch (format) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have |
||
| case 'HH:mm:ss': | ||
| return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` | ||
| case 'HH:mm': | ||
| return `${pad(date.getHours())}:${pad(date.getMinutes())}` | ||
| case 'yyyy-MM-dd HH:mm:ss': | ||
| return date.toLocaleString('sv-SE') // ISO-like format | ||
| case 'MM/dd HH:mm': | ||
| return `${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` | ||
| case 'MMM dd, yyyy': | ||
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) | ||
| case 'relative': | ||
| return formatRelativeTime(date.getTime()) | ||
| case 'auto': | ||
| default: | ||
| return autoDetectTimeFormat(date.getTime()) | ||
| } | ||
| } | ||
|
|
||
| function formatRelativeTime(timestamp: number): string { | ||
| const now = new Date().getTime() | ||
| const diff = now - timestamp | ||
| const absDiff = Math.abs(diff) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which case do we cover with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An edge case where future timestamps may be stored in a table. Like forecasts, for example. |
||
|
|
||
| if (absDiff < 60000) return `${Math.floor(absDiff/1000)}s ago` | ||
| if (absDiff < 3600000) return `${Math.floor(absDiff/60000)}m ago` | ||
| if (absDiff < 86400000) return `${Math.floor(absDiff/3600000)}h ago` | ||
| return `${Math.floor(absDiff/86400000)}d ago` | ||
| } | ||
|
|
||
| function autoDetectTimeFormat(timestamp: number): string { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue, use |
||
| const date = new Date(timestamp) | ||
| const now = new Date().getTime() | ||
| const diff = Math.abs(now - timestamp) | ||
|
|
||
| // Helper function to pad numbers with leading zeros | ||
| const pad = (num: number): string => num.toString().padStart(2, '0') | ||
|
|
||
| if (diff < 3600000) { // < 1 hour: show time with seconds | ||
| return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` | ||
| } else if (diff < 86400000) { // < 1 day: show time without seconds | ||
| return `${pad(date.getHours())}:${pad(date.getMinutes())}` | ||
| } else if (diff < 2592000000) { // < 30 days: show date + time | ||
| return `${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` | ||
| } else { // > 30 days: show date only | ||
| return date.toLocaleDateString('sv-SE').substring(0, 10) // yyyy-MM-dd | ||
| } | ||
| } | ||
|
|
||
| function setDrawBtnToCancel() { | ||
| btnDraw.html('<i class="icon icon-stop"></i><span>Cancel</span>') | ||
| btnDraw.removeClass("js-chart-draw").addClass("js-chart-cancel") | ||
|
|
@@ -137,10 +229,34 @@ export function quickVis( | |
| data[i] = dataset[i][xAxisDataIndex] | ||
| } | ||
|
|
||
| optionXAxis = { | ||
| type: "category", | ||
| name: xAxis, | ||
| data: data, | ||
| // Check if this is a time column | ||
| const timeColumns = detectTimeColumns(columns) | ||
| const isTimeColumn = timeColumns.some(col => col.name === xAxis) | ||
|
|
||
| if (isTimeColumn) { | ||
| // For time columns, format the data and use time axis | ||
| const formattedData = data.map(value => { | ||
| if (isValidTimestamp(value)) { | ||
| return formatTimestamp(value, selectedTimeFormat) | ||
| } | ||
| return value | ||
| }) | ||
|
|
||
| optionXAxis = { | ||
| type: "category", | ||
| name: xAxis, | ||
| data: formattedData, | ||
| axisLabel: { | ||
| rotate: data.length > 10 ? 45 : 0, | ||
| interval: 'auto' | ||
| } | ||
| } | ||
| } else { | ||
| optionXAxis = { | ||
| type: "category", | ||
| name: xAxis, | ||
| data: data, | ||
| } | ||
| } | ||
| } else { | ||
| optionXAxis = {} | ||
|
|
@@ -228,6 +344,7 @@ export function quickVis( | |
| setDrawBtnToCancel() | ||
| requestActive = true | ||
| chartType = chartTypePicker.selected() | ||
| selectedTimeFormat = timeFormatPicker.selected() | ||
|
|
||
| // check if the only change is chart type | ||
| const selectedXAxis = xAxisPicker.selected() | ||
|
|
@@ -295,6 +412,29 @@ export function quickVis( | |
|
|
||
| yAxisPicker.set(x.slice(1).map((item) => item.text)) | ||
|
|
||
| // Time format picker options | ||
| const timeFormatOptions = [ | ||
| { text: "Auto", value: "auto" }, | ||
| { text: "2021-11-21 14:04:09", value: "yyyy-MM-dd HH:mm:ss" }, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The value string is explicit enough so that we can use it in |
||
| { text: "14:04:09", value: "HH:mm:ss" }, | ||
| { text: "14:04", value: "HH:mm" }, | ||
| { text: "11/21 14:04", value: "MM/dd HH:mm" }, | ||
| { text: "Nov 21, 2021", value: "MMM dd, yyyy" }, | ||
| { text: "Relative (5m ago)", value: "relative" } | ||
| ] | ||
| timeFormatPicker.setData(timeFormatOptions) | ||
|
|
||
| // Show/hide time format picker based on time column detection | ||
| const timeColumns = detectTimeColumns(columns) | ||
| const timeFormatGroup = document.querySelector('.time-format-group') | ||
| if (timeFormatGroup) { | ||
| if (timeColumns.length > 0) { | ||
| timeFormatGroup.classList.add('visible') | ||
| } else { | ||
| timeFormatGroup.classList.remove('visible') | ||
| } | ||
| } | ||
|
|
||
| // stash query text so that we can use this later to server for chart column values | ||
| query = data.query | ||
| clearChart() | ||
|
|
@@ -322,6 +462,24 @@ export function quickVis( | |
| echart = echarts.init(viewport, eChartsMacarons) | ||
| eventBus.subscribe(EventType.MSG_QUERY_DATASET, updatePickers) | ||
| btnDraw.click(btnDrawClick) | ||
|
|
||
| // Event listener for X-axis selection to toggle time format visibility | ||
| xAxisPicker.onChange = (info: any) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if i specify a format from format picker, and include a timestamp column in y-axis?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The timestamp values will be displayed as data points. |
||
| if (cachedResponse && cachedResponse.columns) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Time format picker is shown in the first render regardless of the contents of the x-axis and y-axis elements. |
||
| const timeColumns = detectTimeColumns(cachedResponse.columns) | ||
| const isTimeColumn = timeColumns.some(col => col.name === info.value) | ||
| const timeFormatGroup = document.querySelector('.time-format-group') | ||
|
|
||
| if (timeFormatGroup) { | ||
| if (isTimeColumn) { | ||
| timeFormatGroup.classList.add('visible') | ||
| } else { | ||
| timeFormatGroup.classList.remove('visible') | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| clearChart() | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot rely on column name for detecting the timestamp. Types are already enough for this purpose. We have an utility function in
ImportCSVFiles, so you can move it tosrc/utils/...and use it here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll work on this.