feat: DH-18165: Add calendar argument to several dx charts#1122
feat: DH-18165: Add calendar argument to several dx charts#1122jnumainville merged 20 commits intodeephaven:mainfrom
Conversation
There was a problem hiding this comment.
PR Overview
This PR adds support for a calendar argument to several chart types in the dx charts library, along with updates to documentation and tests. Key changes include:
- Adding a calendar parameter to chart APIs and propagating the calendar option to underlying plotly figures.
- Updating documentation for OHLC, candlestick, scatter, line, and area charts to include calendar usage.
- Adjusting tests and internal implementation (e.g. in FigureCalendar, _private_utils, and PartitionManager) for calendar handling.
Reviewed Changes
| File | Description |
|---|---|
| plugins/plotly-express/src/deephaven/plot/express/deephaven_figure/FigureCalendar.py | Introduces the FigureCalendar class and methods to convert calendar data for the frontend. |
| plugins/plotly-express/docs/*.md | Updates documentation for various chart types to include calendar usage examples. |
| plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts | Updates tests to accommodate new calendar behavior and related events. |
| plugins/plotly-express/src/deephaven/plot/express/plots/*.py | Adds calendar parameters to chart plotting functions and updates logic to force svg render mode when a calendar is provided. |
| Other files | Adjustments in data generators, custom draw functions, and internal utils to support the new calendar functionality, plus minor grammar fixes. |
Copilot reviewed 37 out of 37 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (1)
plugins/plotly-express/src/deephaven/plot/express/deephaven_figure/FigureCalendar.py:216
- Overriding dict as a method might cause confusion with the built-in dict attribute. Consider renaming this method to to_dict() to clearly indicate its purpose.
def __dict__(self) -> FigureCalendarDict | None:
| this.calendar = { | ||
| ...calendar, | ||
| timeZone, | ||
| } as unknown as DhType.calendar.BusinessCalendar; |
There was a problem hiding this comment.
Hmmm this isn't quite right... you need to cast it, because the holiday.date is just a string instead of the LocalDateWrapper type that BusinessCalendar has... And this is currently working because we just call holiday.date.toString() in ChartUtils.
At the very least there needs to be some comments about why that's being done. But I'd rather wrap the string in an object that does match the interface correctly, so converting those holiday.dates to something that conforms to:
export interface LocalDateWrapper {
valueOf():string;
getYear():number;
getMonthValue():number;
getDayOfMonth():number;
toString():string;
}
Otherwise we open ourselves up to issues later.
|
plotly-express docs preview (Available for 14 days) |
|
plotly-express docs preview (Available for 14 days) |
|
plotly-express docs preview (Available for 14 days) |
|
There is a small bug where the loading spinner is not dismissed correctly when creating multiple charts I am investigating but otherwise this is ready for review update - this is fixed |
|
plotly-express docs preview (Available for 14 days) |
mofojed
left a comment
There was a problem hiding this comment.
I think I got hung up on some of the diff by changes brought over by indicator merge... but there's still a couple spots that should be cleaned up here.
| @@ -334,6 +335,45 @@ export class PlotlyExpressChartModel extends ChartModel { | |||
| return timeZone !== newTimeZone && newTimeZone != null; | |||
| } | |||
|
|
|||
| /** | |||
| * Update the calendar object from the data | |||
| * @param data The new data to update the calendar from | |||
| */ | |||
| updateCalendar(data: PlotlyChartWidgetData): void { | |||
| const { calendar } = data.figure.deephaven; | |||
There was a problem hiding this comment.
This casting is gross, and then doing a .forEach is pretty nasty when you should just do a .map. Rewritten so you don't need casts (and probably add a name to FigureCalendar so that you don't need to add a name here):
| const newCalendar = { | |
| ...calendar, | |
| timeZone, | |
| } as unknown as DhType.calendar.BusinessCalendar; | |
| // Holidays should be converted to LocalDate objects so | |
| // they have all the necessary methods to match the BusinessCalendar interface | |
| newCalendar.holidays.forEach((holiday, i) => { | |
| const { date } = holiday; | |
| // date is a really a string at this point, but it should be a LocalDate object | |
| const dateObj = new Date(date as unknown as string); | |
| const year = dateObj.getFullYear(); | |
| const month = dateObj.getMonth(); | |
| const day = dateObj.getDate(); | |
| newCalendar.holidays[i] = { | |
| ...newCalendar.holidays[i], | |
| date: { | |
| valueOf: () => date, | |
| getYear: () => year, | |
| getMonthValue: () => month, | |
| getDayOfMonth: () => day, | |
| toString: () => date, | |
| } as unknown as DhType.LocalDateWrapper, | |
| }; | |
| }); | |
| this.calendar = newCalendar; | |
| this.calendar = { | |
| ...calendar, | |
| timeZone, | |
| name: 'PlotlyExpressCalendar', | |
| holidays: calendar.holidays.map((holiday, i) => { | |
| const { date } = holiday; | |
| // date is a really a string at this point, but it should be a LocalDate object | |
| const dateObj = new Date(date as unknown as string); | |
| const year = dateObj.getFullYear(); | |
| const month = dateObj.getMonth(); | |
| const day = dateObj.getDate(); | |
| return { | |
| ...holiday, | |
| date: { | |
| valueOf: () => date, | |
| getYear: () => year, | |
| getMonthValue: () => month, | |
| getDayOfMonth: () => day, | |
| toString: () => date, | |
| } as unknown as DhType.LocalDateWrapper, | |
| }; | |
| }), | |
| }; |
| * @param id The table ID to unsubscribe from | ||
| */ | ||
| unsubscribeTable(id: number): void { | ||
| this.tableSubscriptionMap.get(id)?.close(); |
There was a problem hiding this comment.
Optimization: Opt to do the this.isSubscribed check first - no need to do the work in timeZoneChanged if we're not subscribed.
| if (this.timeZoneChanged(formatter) && this.isSubscribed) { | |
| if (this.isSubscribed && this.timeZoneChanged(formatter)) { |
|
|
||
| /** | ||
| * Check if the data at the selector should be replaced with a single value instead of an array | ||
| * @param data The data to check |
There was a problem hiding this comment.
We shouldn't be casting stuff like this. That's an indication we're doing something wrong; casting should be a last resort, not used liberally like this (particularly casting to any). We're essentially rendering the type checking system useless.
There was a problem hiding this comment.
Not sure why that was so ugly in the first place, done.
| if (formatter != null && calendar != null) { | ||
| const layoutUpdate: Partial<Layout> = {}; | ||
|
|
||
| Object.keys(layout).forEach(key => { |
There was a problem hiding this comment.
Could do a filter here which might be a little clearer, instead of having the if block within, e.g.
Object.keys(layout).filter(key => key.includes('axis')).forEach(key => {
const axis = layout[key as keyof Layout];
|
plotly-express docs preview (Available for 14 days) |
|
plotly-express docs preview (Available for 14 days) |
|
plotly-express docs preview (Available for 14 days) |
Fixes DH-18165
Adds calendar option to several chart types. Some good examples are in the docs and e2e tests.
I also noticed some of the js tests were not quite correct when making my new ones. Since there was just a few straightforward changes I made them in this ticket.