-
Notifications
You must be signed in to change notification settings - Fork 97
Custom theming and scaling #212
-
Hello Alec,
I stumbled across your vue-data-ui component library and my mind was blown - not to mention it's open source. Naturally, I had to give your project a try and I was very happy with it compared to other libraries like apex.
However there are two areas where I am struggling to find a workaround for in order to adopt within the existing architecture. I have a strong feeling you'll have a good idea of how to resolve.
First is theming. To add some background here, I've adopted Vuetify as the core component library within my Vue apps. Hate it or love it, I personally find it extremely well put together and extensible. The issue that arises is how Vuetify handles app level theming versus how vue-data-ui component theming works. I see in the docs there are built in themes, but I wasn't able to figure out how to create dynamic theming in order to match the light/dark mode (or whatever custom theming someone might use in their Vuetify app). I did not try everything under the sun, but things that I thought might work didn't appear to work as expected, but for example I did not try using composables to dynamically reproduce the config of a chart based on the Vuetify theme change. I feel like there's a way to create a config wrapper to do this.
Second is scaling. Basically, the use case is a pure CSS flexing of width and height where a dashboard flexes to fill available space. In my use cases, dashboards are viewed on variable screen sizes and resolutions - which requires components within the chart to scale respectively. However, the scaling of charts with the responsive property tend to scale vue-data-ui components as a vector image rather than, for example, increasing the overall size of the chart while leaving the line, markers, font, labels, etc. unchanged. You can see this if you were to add a component within something like split panes, and the responsive scaling is quite dramatic which ends up shrinking the chart area due to the growth of the legend.
I really appreciate the work you're doing here. I don't understand how it is not more widely known, to be honest!
Thanks,
Tyler
Edit 2025年07月10日: Working solution at the bottom of this discussion.
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
Replies: 8 comments 25 replies
-
Hi @tjones-ieee
Thank you for your support and the food for thought, it's much appreciated:)
Theming
A composable would indeed be the best way to handle dynamic config based on a theme. Here is a sample implementation: https://stackblitz.com/edit/vitejs-vite-8sszo12r?file=src%2FApp.vue
useTheme
manages global state of the themeuseVueDataUiConfig
sets defaults to be used for each chart for each theme.ThemedChart.vue
is a wrapper component used for the purpose of this demonstration
This requires a bit of work, but gets you what you need.
Responsive scaling
I'm not currently satisfied with the responsive state of components. I'm yet to find better ways to handle this.
However, there are workarounds for the legend issue:
- create a custom legend using the #legend slot (see https://vue-data-ui.graphieros.com/customization#legend-slot for an example) that gets applied under a given breakpoint
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
I appreciate your prompt reply on these two topics.
I'll explore the composable approach a little further and let you know what I come up with, but I was hoping there was a way to create a custom theme to apply instead of overriding component configuration. This in particular is something I will have to do in some form regardless of the charting components used.
The response state of components is really where we are struggling in being able to integrate and leverage. I feel relieved you're unsatisfied with the responsive scaling of components, and I imagine it is a very difficult task to change how it's being done. Is there a way to turn off aspects of responsiveness? Are there any users which rely on the responsiveness as-is?
Here's an example of what I am attempting to resolve. It's the same chart with identical config and dataset properties. Naturally, you would expect certain things to be larger, but it tends to appear unbalanced. For example, the X-axis labels are larger in the bottom chart, as too are the markers, but there are no other changes to the look of the chart.
I've experimented with turning "responsive" off and manually setting the chart height/width. Turning responsive off fixes the scaling behavior (and frankly, better respects font size) of markers and font sizes; however, in doing so it introduces additional complications because those properties are not including the title, subtitle, legend, zoom, etc. divs.
I'll keep running tests and exploring things over the next week or so.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
I can't live with the idea of users having to struggle with resizeObservers ^^
Beta Was this translation helpful? Give feedback.
All reactions
-
😄 1
-
Haha fair, they aren't fun. Unfortunately for my use cases I already had to implement it!
I like your approach of using the parent to resize accordingly and handle the internal subtractions for title, subtitle, etc. That may be the quick fix we're after here.
There are some other nuances with the zoom control, perhaps take a look while you're in there? Pertains to the center alignment of the two thumbs.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Yeah I know, I'll check it out too.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
@tjones-ieee
In order to avoid breaking changes, I will add a config option to avoid font sizes and markers resizing in responsive mode, so they stay proportional to the svg. The current responsive feature does most of the job, and it's straightforward to just mute some parts of the code where this resizing occurs.
Would you be willing to try out a beta when it's ready ?
Beta Was this translation helpful? Give feedback.
All reactions
-
Most definitely, sign me up! I will be out the rest of this week with the US holiday.
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1
-
You can try out version 2.13.3 (I had to publish a minor instead of a beta).
In your config for VueUiXy:
const config = computed(() => { return { responsive: true, responsiveProportionalSizing: false, // new. Default: true (previous behavior) //... rest of your config } })
responsiveProportionalSizing
was also added to the following components:
VueUiCandlestick
VueUiFunnel
VueUiHistoryPlot
VueUiParallelCoordinatePlot
VueUiRelationCircle
VueUiStripPlot
VueUiTimer
When set to false, responsiveProportionalSizing
makes components ignore the 'smart' resizing applied by default on fontSizes and other elements.
I did not find a fix for the range handles centering issue. Sometimes centered, sometimes not (wtf).
In the meantime you can force it in your main stylesheet
.vue-data-ui-zoom input[type="range"] { top: 8px !important; }
Let me know how it goes.
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1
-
Hi @graphieros,
Very nice, the responsiveProportionalSizing
property you've added is a promising start.
Testing findings
With responsiveProportionalSizing
set to false, it appears to disable a few aspects of the VueUiXy
config (and probably others as well). The fontSize
properties appear to be primarily impacted for the chart area itself (works as expected in both the legend and tooltip), and sizing of markers (such as line.radius
). For example, the x and y axis labels don't respect the fontSize
property value.
Not sure if this is related, but the x-axis labels change the cursor to pointer (implying there's a "click" option available).
The CSS override for the zoom tool works, with and without the mini-map. Not sure how else to fix without possibly wrapping the inputs within a parent div to do center alignment with.
Apologies, it was a holiday weekend here in the US and I was out of town and left my computer behind.
Tyler
Beta Was this translation helpful? Give feedback.
All reactions
-
Hi @tjones-ieee
Title, Legend, Tooltips, are not part of the chart SVG element, their fontSize is always 'truthful'. Just divs all the way.
However, the size of text elements inside the SVG, (and all other elements painted on the svg) necessarily scale to its viewbox.
Setting responsiveProportionalSizing to false just fallbacks to this default behavior, where all sizes of svg elements are proportional to the viewBox. The original intention with responsivePorportionalSizing to true, was to try and compensate this behavior by artificially scaling the sizes, depending on wether the height or width is the largest.
I probably need to find a better way to handle this, but in the meantime, you can try and set the config.chart.height and width to a sweet spot matching a 'truthful' fontSize for svg text elements (dynamic font size in your config, based on a dynamic config width based on the chart container width would probably do the trick, but again, this is theory).
I'm currently working on other features (out of the box date formatting config options so you can pass timestamps in the time series and have them nicely formatted with your locale), but I'll keep this in mind.
X axis labels have an emit associated which you can capture, but no point in having a pointer if not used. It will be fixed in the next patch, to have the cursor as pointer only if the component consumes the @selectTimeLabel event.
Cheers
Beta Was this translation helpful? Give feedback.
All reactions
-
Funny how in the dev world there's always "something else" lol
Title, Legend, Tooltips, are not part of the chart SVG element, their fontSize is always 'truthful'. Just divs all the way.
Understood, I was just providing input that they didn't appear to be affected. Wasn't entirely sure the extent of the changes you made to scaling.
When you mentioned a resize observer, were you thinking you would find the Vue Data UI component div and then capture the parent from the DOM hierarchy? Or would you use an override property (new flag for responsive sizing) which uses the existing chart height property? Example with the Bullet chart I believe would do the trick:
const noTitle = ref(null); const chartTitle = ref(null); const chartLegend = ref(null); const chartSource = ref(null); // vueuse, if ref is null, height/width is zero const noTitleSize = useElementSize(noTitle); const titleSize = useElementSize(chartTitle); const legendSize = useElementSize(chartLegend); const sourceSize = useElementSize(chartSource); // bound to the SVG height const svgHeight = computed(() => { // chart height from config, or capture the DOM hierarchy if (FINAL_CONFIG...chart.someNewFlag) { // probably don't need this if let titleHeight = 0; if (noTitle.value) { titleHeight = noTitleSize.height.value; } else if (chartTitle.value) { titleHeight = titleSize.height.value; } // make it so the SVG height is what shrinks to fit available space return FINAL_CONFIG...chart.height - FINAL_CONFIG...padding - titleHeight - legendSize.height.value - sourceSize.height.value; } else { // handle existing functionality return FINAL_CONFIG...chart.height; } });
Beta Was this translation helpful? Give feedback.
All reactions
-
Awesome, I hope you're able to find it useful for usage in vue-data-ui! It's an attempt at bringing a "desktop" vibe to a web app.
How are we looking with the chart scaling? The date formatting is slick!
Beta Was this translation helpful? Give feedback.
All reactions
-
To include this component in vue-data-ui, it would need to be stripped of all the dependencies, as it is important for me the lib stays free of deps. That's not impossible to achieve of course, it would even bring the total number of components to 64, which is obviously a cool number.
How are we looking with the chart scaling?
I thought we were good now ?
Beta Was this translation helpful? Give feedback.
All reactions
-
it would need to be stripped of all the dependencies
Indeed. Technically, Vuetify just wraps divs and such with styling. A 64th component would be sweet! I'll see about doing that this week.
Yesterday you mentioned you forgot the else
clauses. But it appears you refactored with the ft-date-formatter merge.
Appreciate your time and effort on this, Alec!
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes, the else clauses were swiftly added, you bet:)
If you want, you can clone vue-data-ui and work on a feature branch for this component; this way your contribution would be recorded. If however you prefer to keep working on the other repo, it's fine too.
Beta Was this translation helpful? Give feedback.
All reactions
-
Fantastic. Now, I just need to finish building out the config wrapper composable. I'll share the code here once it's done.
I'll look into cloning the code and adding it that way. Might proof of concept in the other repo first though.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Sounds good:)
I created a trunk branch you can pull and pr to.
Any changes or fixes made on master on my side will be merged there too.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Vuetify-ing the VueDataUI config
In short, there is currently no way to create a single wrapper to augment every VueDataUI config with Vuetify theme styling. Each configuration must be individually handled. Below is an example for how to do this for the vue-ui-xy component. It is not a complete example, but it provides the necessary steps to unify VueDataUI with Vuetify and can be augmented for your needs.
Credit to Alec for providing some example code.
Note: the mergeObjects function is the same as the mergeConfigs
except I've adopted it for other uses.
import { mergeConfigs } from 'vue-data-ui';
import { computed, type Ref } from 'vue'; import { type VueUiXyConfig } from 'vue-data-ui'; import { useTheme } from 'vuetify'; //import { mergeObjects } from '@/core/utils'; function isPlainObject(v: any) { return v !== null && typeof v === 'object' && !Array.isArray(v); } /** * Merge two objects together into a new object with the unique properties from both. * Properties from ```obj2``` will overwrite properties from ```obj1```. * * @param obj1 The primary object to merge. * @param obj2 The secondary object to merge with the first. * @returns A new merged object containing all properties from obj1 and obj2. */ export function mergeObjects(obj1: any = {}, obj2: any = {}) { const out: any = {}; for (const key of new Set([...Object.keys(obj1), ...Object.keys(obj2)])) { const dv = obj1[key], uv = obj2[key]; if (isPlainObject(dv) && isPlainObject(uv)) { out[key] = mergeObjects(dv, uv); } else { /* if key in obj2 is defined, even if its value is undefined, we want to preserve this. In other words, property value within obj2 is explicitly set to a value of some form, and we should override the output (which takes precedence the property value of obj1). */ if (key in obj2) { out[key] = uv; } else { out[key] = dv; } } } return out; } type iDefaultConfig = { /** The background color based on the Vuetify theme */ bg: string; /** The font color based on the Vuetify theme */ text: string; /** The stroke color (for grid lines) based on the Vuetify theme */ stroke: string; }; /** * Wrapper composable to synchronize VueDataUI XY Chart with Vuetify's light and dark themes. * * @param customConfig The reactive VueDataUI component configuration object. * @returns ```config``` - the reconstructed VueDataUI configuration object matching the Vuetify theme. * * @example * const defaultConfig = ref<VueUiXyConfig>(getVueDataUiConfig('vue_ui_xy') as VueUiXyConfig); * const { config } = useToVuetify(defaultConfig); * // load config from the component config into defaultConfig... */ export function useToVuetify(customConfig: Ref<any>) { const theme = useTheme(); const themeName = computed<'light' | 'dark'>(() => { if (theme.current.value.dark) return 'dark'; return 'light'; }); /* The colors to align VueDataUI with Vuetify's light/dark themes. */ const colors = computed<iDefaultConfig>(() => { return { // bg: { light: 'transparent', dark: 'transparent' }[themeName.value], bg: theme.current.value.colors.surface, text: { light: '#1a1a1a', dark: '#f5f5f5' }[themeName.value], stroke: { light: '#afb0b0', dark: '#8f9090' }[themeName.value] }; }); /* The configuration that's synchronized with Vuetify's theme. */ const vuetifyConfig = computed(() => { return { responsive: true, responsiveProportionalSizing: false, chart: { backgroundColor: colors.value.bg, color: colors.value.text, height: null, // must use null, not undefined width: null, grid: { stroke: colors.value.stroke, frame: { stroke: colors.value.stroke }, labels: { color: colors.value.text, xAxisLabels: { color: colors.value.text } } }, highlighter: { color: colors.value.stroke }, highlightArea: { color: colors.value.stroke, caption: { color: colors.value.text } }, legend: { color: colors.value.text }, timeTag: { backgroundColor: colors.value.bg, color: colors.value.text }, title: { color: colors.value.text, subtitle: { color: colors.value.text } }, tooltip: { color: colors.value.text, backgroundColor: colors.value.bg, backgroundOpacity: 5, borderColor: colors.value.stroke }, zoom: { show: false } }, line: { labels: { color: colors.value.text } } }; }); const config = computed<VueUiXyConfig>(() => { const custom = customConfig.value || {}; const vuetify = vuetifyConfig.value || {}; return mergeObjects(custom, vuetify); }); return { config }; }
Usage example
const yourConfig = ref<VueUiXyConfig>(getVueDataUiConfig('vue_ui_xy') as VueUiXyConfig); // implement whatever additional styling you require const { config } = useToVuetify(yourConfig);
<VueUiXy :dataset="dataset" :config="config" />
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Beta Was this translation helpful? Give feedback.