How to pass DOM elements for libraries (eg. ChartJS, Hightcharts) in Virtual DOMs (such as Qwik)? - dom

Background
I have personally used React, Vue and Angular extensively in the past. And a lot of times I need to create applications with charts generated within them from selective data. I'm recently trying out Qwik due to its promise of speed and attempted to create charts within it using ChartJs. But while ChartJs has separate libraries available for React, Vue, Angular, Svelte, etc. it does not have one for Qwik understandably.
Issue
Many plugins such as Highcharts and ChartJs often require a DOM element to be sent to its functions to identify where to render their output. But when we are dealing with virtual DOMs, I can't run JS selector scripts to fetch DOM elements and pass them into a function within a component. Therefore, as of now, I have not been able to use ChartJs in my Qwik project.
Attempts
I have only looked for solutions for this issue and not found any workable approaches. From ChartJs docs the following code is their raw JS way of implementing charts:
new Chart(
document.getElementById('acquisitions'),
{
type: 'bar',
data: {
labels: data.map(row => row.year),
datasets: [
{
label: 'Acquisitions by year',
data: data.map(row => row.count)
}
]
}
}
);
As expected document.getElementById does not work inside a component and that is where I'm stuck. I've only created the useMount$() function where I expect to place the logic for generating my chart and also looked around for React solutions by perhaps using references and what not. But, other than that, I have been unable to find anything more.
I understand that looking at the source code of the React library for ChartJs would provide me clues but while I investigate a library (which I find difficult at my current level) I was hoping for a pointer to the solution from the Stack Overflow community.
Searching "ref" on the Qwik docs does not return any search results but I had found the git project from another developer online and tried to replicate the use of references from his approach:
Child component code:
import { component$, useMount$, Ref, useStylesScoped$ } from "#builder.io/qwik";
import { Chart } from 'chart.js/auto';
interface GraphProps {
data: object[];
reference: Ref<Element>;
}
export default component$((props: GraphProps) => {
useStylesScoped$(styles);
useMount$(() => {
new Chart(
props.reference.value,
{
<... options here ...>
}
);
});
return (
<div id="chartContent">
</div>
);
});
Parent component code:
import { component$, useRef } from "#builder.io/qwik";
import ContentCard from "../components/contentCard/contentCard";
import ChartJSGraph from "../components/chartJSGraph/chartJSGraph";
...
export default component$(() => {
const leftChartContainer = useRef();
return (
<div>
<div className="row">
<ContentCard>
<div className="graph-container">
<ChartJSGraph
data={[
{ year: 2010, count: 10 },
...
]}
reference={leftChartContainer}
/>
</div>
</ContentCard>
</div>
</div>
)
});
As these are just findings from a YouTuber's code it could be outdated so is certainly not necessarily a reliable source. But so far searching the official docs have not led me to any official approach for references.

The DOM element that is passed to the charting library can only be accessed once it has been mounted to the page. Qwik/Vue/React all provide component mounted hooks.
https://qwik.builder.io/docs/components/lifecycle/#usemount
https://vuejs.org/api/composition-api-lifecycle.html#onmounted
https://reactjs.org/docs/react-component.html#componentdidmount
Inside these mounted hooks you can reference your DOM element via id or querySelector or using the internal DOM reference feature of Qwuik/Vue/React and then use that when initialising the chart. The latter is the cleaner approach.
For example, in Vue:
<template>
<div id="acquisitions" ref="chartEl"></div>
</template>
<script setup>
import Chart from 'chart.js/auto';
import { ref, onMounted } from 'vue';
const chartEl = ref(null)
onMounted(() => {
const chartOpts = {
type: 'bar',
data: {
labels: data.map(row => row.year),
datasets: [
{
label: 'Acquisitions by year',
data: data.map(row => row.count)
}
]
}
}
new Chart(
chartEl.value,
chartOpts
);
})
</script>

Solution
Sadly this was a silly issue of perhaps on my network side or god knows what why the search engine on the Qwik doc never suggested anything for me when I looked up "Ref" in their docs. But my problem has been solved after finding the following link:
https://qwik.builder.io/tutorial/hooks/use-signal/#example
For future reference for myself or any beginners facing the similar issue, I'm writing down my implementation below:
// Root component
import { component$, useSignal } from "#builder.io/qwik";
...
import ChartJSGraph from "../components/chartJSGraph/chartJSGraph";
export default component$(() => {
const chartData1 = useSignal({
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
label: 'Inventory Value per Outlet',
data: [65, 59, 80, 81, 56, 55, 40],
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
});
return (
<div class="w-100 h-100">
...
<ChartJSGraph
width={'100%'}
height={'25px'}
chartData={chartData1.value}
/>
</div>
);
});
And here's the code for my ChartJSGraph component that uses the data supplied to generate the chart while using the reference of the canvas element to point to ChartJS where to create the chart.
// ChartJSGraph component
import { component$, useClientEffect$, useSignal } from "#builder.io/qwik";
import { Chart } from 'chart.js/auto';
...
interface GraphProps {
height: string;
width: string;
chartData: object;
}
export default component$((props: GraphProps) => {
const outputRef = useSignal<Element>();
useClientEffect$(() => {
new Chart(
outputRef.value,
{
type: 'line',
data: props.chartData
}
);
});
return (
<>
<canvas ref={outputRef} width={props.width} height={props.height}>
</canvas>
</>
);
});

Related

How to use Mapbox geocoder with Quasar select?

I'm trying to create an Autocomplete component using the Mapbox Geocode API and Quasar's <q-select /> component. It appears though that Mapbox requires using their input (could be wrong about this), so I'm having trouble hooking it up to the select.
I've tried using the #mapbox/mapbox-gl-geocoder, vue-mapbox-ts and v-mapbox-geocoder libraries now. The two third-party libraries had some issues with them, so I'd prefer to use the one direct from Mapbox if possible.
<template>
<q-select
v-model="state.location"
:options="state.locations?.features"
:option-value="(result: MapboxGeocoder.Result) => result.place_name"
:option_label="(result: MapboxGeocoder.Result) => result.place_name"
:loading="state.loadingResults"
clear-icon="clear"
dropdown-icon="expand_more"
clearable
outlined
use-input
dense
label="Location">
<template #prepend>
<q-icon name="place " />
</template>
</q-select>
</template>
<script lang='ts' setup>
import { reactive, ref, onMounted } from 'vue';
import MapboxGeocoder from '#mapbox/mapbox-gl-geocoder';
const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN as string;
const state = reactive({
first_name: auth.currentUser?.first_name || undefined,
last_name: auth.currentUser?.last_name || undefined,
location: undefined,
locations: undefined as undefined | MapboxGeocoder.Results,
loadingResults: false,
geocoder: null as null | MapboxGeocoder,
});
onMounted(() => {
state.geocoder = new MapboxGeocoder({
accessToken,
types: 'country,region,place,postcode,locality,neighborhood',
});
state.geocoder?.on('result', (e) => {
console.log('on result: ', e);
});
state.geocoder?.on('results', (e) => {
console.log('results: ', e);
state.locations = e.features;
});
state.geocoder?.on('loading', (e) => {
console.log('loading');
state.loadingResults = true;
});
});
</script>
In the code sample above, none of the console logs are being run. If I add an empty <div id="geocoder" /> and then use the state.geocoder.addTo('#geocoder') function, it renders the Mapbox input and hits the console logs, but then I am unable to use the Quasar select like I'm hoping to.
How can I go about accomplishing this?
I never tracked down the reason why your seemingly correct syntax failed, but if I used this alternative:
const function results(e) {
console.log('results: ', e);
state.locations = e.features;
}
state.geocoder?.on('results', results);
everything magically worked.
MapboxGeocoder is a UI control, it's not meant to be used in a "headless" mode.
As you create your own control, you could just use the Mapbox Geocoder API, see https://docs.mapbox.com/api/search/geocoding/ for more information on how this works.

SvelteKit console error "window is not defined" when i import library

I would like to import apexChart library which using "window" property, and i get error in console.
[vite] Error when evaluating SSR module /src/routes/prehled.svelte:
ReferenceError: window is not defined
I tried use a apexCharts after mount, but the error did not disappear.
<script>
import ApexCharts from 'apexcharts'
import { onMount } from 'svelte'
const myOptions = {...myOptions}
onMount(() => {
const chart = new ApexCharts(document.querySelector('[data-chart="profit"]'), myOptions)
chart.render()
})
</script>
I tried import a apexCharts when i am sure that browser exist.
import { browser } from '$app/env'
if (browser) {
import ApexCharts from 'apexcharts'
}
But i got error "'import' and 'export' may only appear at the top level"
I tried disable ssr in svelte.config.js
import adapter from '#sveltejs/adapter-static';
const config = {
kit: {
adapter: adapter(),
prerender: {
enabled: false
},
ssr: false,
}
I tried to create a component in which I import apexChart library and I created a condition that uses this component only if a browser exists
{ #if browser }
<ProfitChart />
{ /if }
Nothing helped.
Does anyone know how to help me please?
The easiest way is to simply include apexcharts like a standalone library in your webpage like this:
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
And then simply use it in the onMount:
onMount(() => {
const chart = new ApexCharts(container, options)
chart.render()
})
You can add this line either in your app.html or include it where it's required with a <svelte:head> block.
An alternative way would be to dynamically import during onMount:
onMount(async () => {
const ApexCharts = (await import('apexcharts')).default
const chart = new ApexCharts(container, options)
chart.render()
})
As an extra: use bind:this instead of document.querySelector to get DOM elements, that would be the more 'svelte' way.
I have found the last option with the Vite plugin to work best with less code in the end but will lose intellisense in vscode and see import highlighted as error (temp workaround at end): https://kit.svelte.dev/faq#how-do-i-use-x-with-sveltekit-how-do-i-use-a-client-side-only-library-that-depends-on-document-or-window
Install vite plugin: npm i -D vite-plugin-iso-import
Add plugin to svelte.config.js:
kit: {
vite: {
plugins: [
isoImport(),
],
Add plugin to TypeScript config (if you use TS):
"compilerOptions": {
"plugins": [{ "name": "vite-plugin-iso-import" }],
Use as normal but note the "?client" on the import:
<script context="module">
import { chart } from 'svelte-apexcharts?client';
import { onMount } from 'svelte'
let myOptions = {...myOptions}
onMount(() => {
myOptions = {...updated options/data}
});
</script>
<div use:chart={myOptions} />
Debugging note:
To have import not highlighting as an error temporarily, just:
npm run dev, your project will compile fine, then test in browser to execute at least once.
remove ?client now, save and continue debugging as usual.
For all of you trying to import dynamically into a js or ts file, try the following:
Import your package during on mount in any svelte component.
onMount(async () => {
const Example = await import('#creator/examplePackage');
usePackageInJSOrTS(Example.default);
});
Use the imported package in your js/ts function. You need to pass the default value of the constructor.
export function usePackageInJsOrTs(NeededPackage) {
let neededPacakge = new NeededPackage();
}

vue-chartjs and custom legend using generateLegend()

The generateLegend() wrapper does call the legendCallback defined in my Vue code but I'm lost to how to render the custom HTML in vue-chartjs. What do I do with htmlLegend as described in the vue-chartjs api docs like here.
Here is the line chart component I'm trying to render with a custom HTML object.
import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: Line,
mixins: [reactiveProp],
props: ['chartData','options'],
data: () => ({
htmlLegend: null
}),
mounted () {
this.renderChart(this.chartData, this.options);
this.htmlLegend = this.generateLegend();
}
}
Here is my vue template
<template>
<div class="col-8">
<line-chart :chart-data="datacollection" :options="chartOptions"></line-chart>
</div>
</template>
Well, htmlLegend holds the markup of the generated legend... so you can just put it into your tag via v-html
<template>
<div class="col-8">
<div class="your-legend" v-html="htmlLegend" />
<line-chart :chart-data="datacollection" :options="chartOptions"></line-chart>
</div>
</template>
mounted() {
this.renderChart( this.chartData , this.options );
var legend = this.generateLegend();
this.$emit('sendLegend', legend)
}
and then in the vue file add a new div to show the legend and also listen to the event to get the legend data
<div class="line-legend" v-html="chartLegend"></div>
<line-chart #sendLegend="setLegend" :chart-data="datacollection" :options="chartOptions"></line-chart>
and also add this to the data
chartLegend: null,
and you also need a method
setLegend (html) {
this.chartLegend = html
},

How to add a Select addon to input field in Semantic UI React?

I am using Semantic UI React for my React JS project. I need to add an addon to the input field, but cant do it.
(ideally the addon would be on the right side)
Anyone can help me?
Thanks
The Select component is a wrapper around Dropdown. I think that you're looking for this example.
I also made a working example for you:
import React from 'react'
import { Dropdown, Input } from 'semantic-ui-react'
const options = [
{ key: '86', text: '+86', value: '86' },
{ key: '89', text: '+89', value: '89' },
]
const Example = () => (
<Input
label={{
basic: true,
content: <Dropdown compact options={options} defaultValue='86' />
}}
placeholder='1234'
/>
)

MaterialUI together with styled-components, SSR

I'm building a new project with SSR using Next.js, MaterialUI and styled-components. From what I know, MaterialUI uses JSS as a tool for SSR (according to the example in its repository). I wonder if anyone knows how I can make it work with styled-components. I opened issues in MaterialUI and styled-components repositories, both authors answered me that they don't know how to make it work together. But probably anyone did it already? Or at least can tell me where to dig to solve this problem. Thanks in advance!
You can use styled-components with material ui, but you'll end up needing to use !important a lot. Like this:
import Button from "material-ui/Button"
const MyButton = styled(Button)`
background: red !important;
`
In the project I'm working on with the same combo, I've just resorted to using the JSS style material-ui wants you to use with the whole withStyles HOC..
You may check their docs here https://material-ui.com/guides/interoperability/#styled-components, you may check the deeper elements section if you want to override specific classes https://material-ui.com/guides/interoperability/#deeper-elements
below is my example where for the switch component
const StyledSwitch = styled(({ ...other }) => (
<div>
<Switch
{...other}
classes={{ colorSecondary: 'colorSecondary', checked: 'checked', bar: 'bar' }}
/>
</div>
))`
& .colorSecondary.checked + .bar {
background-color: ${props => props.theme.lighter.toString()};
}
& .colorSecondary.checked {
color: ${props => props.theme.default.toString()};
}
`;
export default StyledSwitch;
usage
<StyledSwitch theme={lightTheme.secondary} />
this is using a theme but you can specify any color you want
Looks like we have 3 ways (could be easier, but not everything is flowers) to override Material UI styles with Styled Components. Here is my Gist.
I do it like this:
In head component of app:
const styleNode = document.createComment('insertion-point-jss')
document.head.insertBefore(styleNode, document.head.firstChild)
const generateClassName = createGenerateClassName()
const jss = create({
...jssPreset(),
insertionPoint: 'insertion-point-jss'
})
<JssProvider jss={jss} generateClassName={generateClassName}>
<Main />
</JssProvider>
and then just style:
import styled from 'styled-components'
import Select from '#material-ui/core/Select'
import Input from '#material-ui/core/Input'
import React from 'react'
export const InputM = styled(({ ...other }) => (
<Input {...other} classes={{ input: 'input' }} />
))`
color: ${p => p.theme.textColor};
& .icon {
font-family: ${p => p.theme.fontFamily};
font-size: ${p => p.theme.fontSize}px;
color: ${p => p.theme.textColor};
}
`