Skip to content

Conversation

fhqvst
Copy link
Contributor

@fhqvst fhqvst commented Sep 25, 2025

Summary

Initially based on #2986, this PR adds support for "resolution" and "lookback" selection in the chart component. The chart will now also backfill more data in the correct resolution as the user is panning/zooming.

image

How has this been tested?

  • Current tests cover my changes
  • Added new tests
  • Manually tested the code

Todo

  • Add back confidence intervals
  • Fix off-by-one errors in data fetching
  • Update styling to fit new design (mostly colors)
  • Self review

Future work

  • Add loading state
  • Move to OHLC charts
  • Add auto-resolution selection

Copy link

linear bot commented Sep 25, 2025

Copy link

vercel bot commented Sep 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
component-library Ready Ready Preview Comment Oct 2, 2025 6:09pm
insights Ready Ready Preview Comment Oct 2, 2025 6:09pm
5 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
api-reference Skipped Skipped Oct 2, 2025 6:09pm
developer-hub Skipped Skipped Oct 2, 2025 6:09pm
entropy-explorer Skipped Skipped Oct 2, 2025 6:09pm
proposals Skipped Skipped Oct 2, 2025 6:09pm
staking Skipped Skipped Oct 2, 2025 6:09pm

@fhqvst fhqvst self-assigned this Sep 25, 2025
@fhqvst fhqvst force-pushed the fhqvst/ui-187-allow-date-range-selection-in-charts branch from b0122ad to 57d034c Compare September 25, 2025 15:32
@vercel vercel bot temporarily deployed to Preview – proposals September 25, 2025 15:32 Inactive
@vercel vercel bot temporarily deployed to Preview – api-reference September 25, 2025 15:32 Inactive
@vercel vercel bot temporarily deployed to Preview – developer-hub September 25, 2025 15:32 Inactive
@vercel vercel bot temporarily deployed to Preview – entropy-explorer September 25, 2025 15:32 Inactive
@vercel vercel bot temporarily deployed to Preview – staking September 25, 2025 15:32 Inactive
@fhqvst fhqvst force-pushed the fhqvst/ui-187-allow-date-range-selection-in-charts branch from 57d034c to 7af9a70 Compare September 26, 2025 15:42
@vercel vercel bot temporarily deployed to Preview – api-reference September 26, 2025 15:42 Inactive
@vercel vercel bot temporarily deployed to Preview – developer-hub September 26, 2025 15:42 Inactive
@vercel vercel bot temporarily deployed to Preview – staking September 26, 2025 15:42 Inactive
@vercel vercel bot temporarily deployed to Preview – proposals September 26, 2025 15:42 Inactive
@vercel vercel bot temporarily deployed to Preview – entropy-explorer September 26, 2025 15:42 Inactive
@fhqvst fhqvst force-pushed the fhqvst/ui-187-allow-date-range-selection-in-charts branch from 7af9a70 to 65f2059 Compare September 26, 2025 15:45
@vercel vercel bot temporarily deployed to Preview – entropy-explorer September 26, 2025 15:45 Inactive
@vercel vercel bot temporarily deployed to Preview – developer-hub September 26, 2025 15:45 Inactive
@vercel vercel bot temporarily deployed to Preview – staking September 26, 2025 15:46 Inactive
@vercel vercel bot temporarily deployed to Preview – api-reference September 26, 2025 15:46 Inactive
@vercel vercel bot temporarily deployed to Preview – proposals September 26, 2025 15:46 Inactive
@fhqvst fhqvst force-pushed the fhqvst/ui-187-allow-date-range-selection-in-charts branch from 65f2059 to 6f95944 Compare October 1, 2025 13:19
@vercel vercel bot temporarily deployed to Preview – api-reference October 1, 2025 13:19 Inactive
@fhqvst fhqvst force-pushed the fhqvst/ui-187-allow-date-range-selection-in-charts branch from 6f95944 to cffaba0 Compare October 2, 2025 13:58
@vercel vercel bot temporarily deployed to Preview – proposals October 2, 2025 13:58 Inactive
@vercel vercel bot temporarily deployed to Preview – api-reference October 2, 2025 13:58 Inactive
@vercel vercel bot temporarily deployed to Preview – staking October 2, 2025 13:58 Inactive
@vercel vercel bot temporarily deployed to Preview – developer-hub October 2, 2025 13:58 Inactive
@vercel vercel bot temporarily deployed to Preview – entropy-explorer October 2, 2025 13:58 Inactive
@vercel vercel bot temporarily deployed to Preview – developer-hub October 2, 2025 18:05 Inactive
@vercel vercel bot temporarily deployed to Preview – staking October 2, 2025 18:05 Inactive
@vercel vercel bot temporarily deployed to Preview – api-reference October 2, 2025 18:05 Inactive
@vercel vercel bot temporarily deployed to Preview – proposals October 2, 2025 18:05 Inactive
@vercel vercel bot temporarily deployed to Preview – entropy-explorer October 2, 2025 18:05 Inactive
@fhqvst fhqvst marked this pull request as ready for review October 2, 2025 18:05
@fhqvst fhqvst requested a review from a team as a code owner October 2, 2025 18:05
@fhqvst fhqvst requested review from cprussin and alexcambose October 2, 2025 18:06
Copy link
Collaborator

@cprussin cprussin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, looking really good! A few comments but nothing super critical

const { symbol, publisher, cluster, from, to, resolution } = parsed.data;

try {
checkMaxDataPointsInvariant(from, to, resolution);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'm not a fan of functions with hidden side effects. I also think the implementation of checkMaxDataPointsInvariant is unnecessarily awkward -- I prefer to avoid the unnecessary use of let bindings, and it's less concise than it needs to be. Additionally, the current implementation is using exceptions as control flow which I generally try to avoid.

How something like this instead?

if ((to - from) / RESOLUTION_TO_SECONDS[resolution] > MAX_DATA_POINTS) {
  return new Response("Unsupported resolution for date range", { status: 400 });
}

// ...

const ONE_MINUTE_IN_SECONDS = 60;
const ONE_HOUR_IN_SECONDS = 60 * ONE_MINUTE_IN_SECONDS;
const RESOLUTION_TO_SECONDS = {
  "1 MINUTE": ONE_MINUTE_IN_SECONDS,
  "5 MINUTE": 5 * ONE_MINUTE_IN_SECONDS,
  "1 HOUR": ONE_HOUR_IN_SECONDS,
  "1 DAY":  24 * ONE_HOUR_IN_SECONDS,
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would you feel about doing checkMaxDataPointsInvariant(...): boolean instead?

if (!checkMaxDataPointsInvariant()) {
   return new Response("Unsupported resolution")
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's reasonable to extract to a separate function but checkMaxDataPointsInvariant is IMO a pretty bad name, how about either getNumDataPoints(to, from, resolution) > MAX_DATA_POINTS or maybe isNumDataPointsOverMax(to, from, resolution)? I personally think getNumDataPoints is simpler / more obvious.

const [resolution, setResolution] = useChartResolution();

const handleLookbackChange = useCallback(
(newValue: Key) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any reason to use Key here -- if you make the resolutions an enum or a string union, the types in <Select> should work out that you can take the enum / union here and you don't have to type guard or cast

Copy link
Contributor Author

@fhqvst fhqvst Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc it did not wanna play nice with enums, but i'll have another look

i think the right thing to do is for Select to take a K generic (perhaps one that extend string | number)? maybe not

Copy link
Collaborator

@cprussin cprussin Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think enums should be OK since we're using them elsewhere e.g. here, and here is an example of where we use it with a union in case you choose that.

I wouldn't use a string | number here; IMO a select should nearly always map to an enum or a union -- there are a restricted set of valid values and string | number implies a freeform input. Not only is the input not freeform so string | number doesn't accurately describe the values it produces, but also if that's what you use then you'll unnecessarily need to do validation & type checking OR type casting. Representing things as an enum/union is both more accurate and will type check correctly without additional checks.

const chartRef = useRef<ChartRefContents | undefined>(undefined);
const isBackfilling = useRef(false);
const priceFormatter = usePriceFormatter();
const resolutionRef = useRef(resolution);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this ref and why not just use resolution directly everywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no point - i think we can delete this now

});

fetch(url, { signal: abortControllerRef.current.signal })
.then(async (data) => historicalDataSchema.parse(await data.json()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should mix async/await and then, just move the schema parsing down to the next .then instead

// Get the current historical price data
const currentHistoricalPriceData = chartRef.current.price
.data()
.filter((d) => isLineData(d));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear on why you need to filter these arrays, can you add a brief comment explaining why isLineData is necessary and what kind of data the data() function might return which wouldn't be line data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me double check this, but i think price.data() returns an intersection of many types of Data (something like AreaData | LineData | ... which is why i needed that type guard

});

const confidenceHigh = chart.addSeries(AreaSeries, confidenceConfig);
const confidenceLow = chart.addSeries(AreaSeries, confidenceConfig);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you moved to using an AreaSeries and don't need to draw the two lines, could you compress this into a single series centered on the price instead of a series above and one below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we can do this if we want the purple background between the lines, but i can have another look

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah understood

export type Resolution = (typeof RESOLUTIONS)[number];

export const LOOKBACKS = ["1m", "1H", "1D", "1W", "1M"] as const;
export type Lookback = (typeof LOOKBACKS)[number];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like the name "lookbacks" for this -- if we do the auto-resolution adjust, this is more accurately a "snap to" than a lookback. Even now I think this is effectively behaving as a "snap to"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep yep, i don't have a strong opinion here. another candidate was INTERVAL, but i found that it could be easily confused with RESOLUTION. what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the best I have in mind is SNAP_TO_PERIODS. I'm sure there's a better name, but INTERVAL doesn't seem clear to me either. Maybe @alexcambose has some ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QUICK_SELECT_WINDOWS maybe?

queryParams.publisher = publisher;
}
if (cluster === "pythtest") {
queryParams.clusters = ["pythnet", "pythtest-conformance"];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why we would fetch from both clusters when pythtest is specified? It also does not look like the cluster is ever passed in so why even have this as a config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this in an artifact from here, but for some reason i've messed it up (pythnet instead of pythtest)... and tbh i'm not even sure we wanna do this at all.

i'll remove the addition of pythtest-conformance so that cluster can only be pythnet or pythtest, and we can add back any conformance-related stuff later.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so to be clear pythtest is not a cluster name, we sometimes mix the names pythtest and pythtest-conformance but pythtest-conformance is the proper name.

I don't think you will ever have a good reason to display a price chart for the pythtest-conformance aggregate -- the aggregate price in pythtest-conformance doesn't actually mean anything since the publishers use that environment to test their feeds and so the data quality is wildly inaccurate. I'd probably just get rid of this. But if we find there is in fact some reason to bring this back, the clusters will be pythnet and pythtest-conformance, not pythtest which does not in fact correspond to a real cluster.

AND publisher = ''
GROUP BY time
ORDER BY time ASC
PREWHERE
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not sure I understand the reason for splitting the where clause and using prewhere here, does postgres not optimize this query well?

Copy link
Contributor Author

@fhqvst fhqvst Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to really get to the bottom here we should probably measure a couple different queries and see which one is faster.

that said, the (cluster, publisher, symbol) combo follows the specified physical sort order of the table, and so forcing the engine to filter by that first should be some kind of guarantee that we're always doing lookups as fast as possible.

slight chance that this is a premature optimization, but it's so small i figured adding it couldnt hurt

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fine, it doesn't bother me to have it if you think it could help with perf (and has no risk of having the opposite impact by accident), but I also doubt this is going to do anything differently than what the query plan optimizer would do anyways...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants