Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Custom theming and scaling #212

tjones-ieee started this conversation in Ideas
Jul 3, 2025 · 8 comments · 25 replies
Discussion options

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.

You must be logged in to vote

Replies: 8 comments 25 replies

Comment options

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 theme
  • useVueDataUiConfig 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:

You must be logged in to vote
0 replies
Comment options

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.

Screenshot 2025年07月03日 120846

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.

You must be logged in to vote
8 replies
Comment options

I can't live with the idea of users having to struggle with resizeObservers ^^

Comment options

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.

Comment options

Yeah I know, I'll check it out too.

Comment options

@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 ?

Comment options

Most definitely, sign me up! I will be out the rest of this week with the US holiday.

Comment options

@tjones-ieee

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.

You must be logged in to vote
1 reply
Comment options

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

Comment options

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

You must be logged in to vote
0 replies
Comment options

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;
 }
});
You must be logged in to vote
16 replies
Comment options

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!

Comment options

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 ?

Comment options

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!

Comment options

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.

Comment options

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.

Comment options

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.

You must be logged in to vote
0 replies
Comment options

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" />
You must be logged in to vote
0 replies
Comment options

Follow-ups to this discussion:

#226
#234

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
enhancement New feature or request bug : layout Chart layout presents a defect advanced usage

AltStyle によって変換されたページ (->オリジナル) /