Client-only Nuxt 3 D3 plugin - d3.js

I'm trying to use the D3 extension in a Nuxt 3 project and for that I created a d3.client.js file in the plugins/ directory.
import * as d3 from "d3";
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(d3)
})
However, when I try to use it gives me a 500 Internal Server Error document is not defined.
<script>
import * as d3 from "d3";
export default {
name: "globe",
created() {
d3.select("#globe");
}
}
</script>
How can I solve this?

d3.select() uses document.querySelector() under the hood. Since you're working server side, you don't have access to document yet. So you'll need to mock it or to avoid using it.
You can avoid using it all together by passing an element instead of a string to d3.select(), as it will then create a functioning d3 selection without running document.querySelector(). And since every other chained .select() or .selectAll() uses previousSelection.querySelector(), you can just continue from there.
If you do not have access to the DOM element directly, you might want to mock document. This article suggests using JSDOM:
import { JSDOM } from 'jsdom';
// create a new JSDOM instance for d3-selection to use
const document = new JSDOM().window.document;
d3.select(document.body)
.append('div');

I managed to solve it by using the d3.select with a Vue reference.
const globe = d3.select(this.$refs.globe)

Related

How to build a Model Layer in Vue3 just like other MVC language?

my name is DP, I have 2 years Vue2 experience, but I am new to Vue3. I am learning Vue3 recently, as I found the "setup(Composition API)" just like the "Controller(in MVC)" that I did in other language, so I am trying to build my test Vue3 project in MVC way, but I go some problem can anyone help? thx!
MVC Plan
M - use class
V - use <template> ... </template>
C - use setup
My Problem
working: using loadTopic_inSetup().then() in setup is working, because topicList_inSetup is defined in setup() too.
not working: using loadTopic_inModel() in setup is not working, I guess some kind data keep problem, because in console I can see the data already got from API
as u can see, I am not expert for js/ts, I am a backend developer, so if you know how to do it, plz help thx very much.
BTW, VUE is greet, I love it.
My Code
//APIBased.ts
import { ajax } from "#/lib/eeAxios"
export class APIBased {
//load data with given url and params
loadData(apiPath: string, params?: object): Promise<any> {
apiPath = '/v1/'+apiPath
return ajax.get(apiPath, params)
}
}
//Topic.ts
import { APIBased } from "./APIBased";
import { ref } from 'vue'
export class Topic extends APIBased {
//try keep data in model
topicList: any = ref([]);
constructor() {
super()
}
//direct return ajax.get, let setup do the then+catch
loadTopic_inSetup() {
return super.loadData('topics', { t_type_id: 1 })
}
//run ajax get set return data to this.topicList, keep data in model
loadTopic_inModel() {
super.loadData('topics', { t_type_id: 1 }).then((re) => {
console.log(re.data)
this.topicList = re.data
})
}
}
//EETest.vue
<template>
<EELayoutMainLayout>
<template v-slot:mainContent>
<h1>{{ "Hello Vue3 !!" }}</h1>
<hr/>
{{to.topicList}} //not working... just empty array
<hr/>
{{topicList_inSetup}} //working... topic list return from API show here.
</template>
</EELayoutMainLayout>
</template>
<script lang="ts">
import { defineComponent, getCurrentInstance, ref } from 'vue'
import EELayoutMainLayout from '#/components/eeLayout/EELayoutMainLayout.vue'
import { Topic } from "#/models/Topic";
export default defineComponent({
name: 'EETest',
props: {
},
setup() {
let topicList_inSetup = ref([])
const to = new Topic()
//try keep data in setup, it's working
to.loadTopic_inSetup().then((re) => {
topicList_inSetup.value = re.data
console.log(re.data)
})
//try keep data in model, the function is run, api return get, but data not show, even add ref in model
to.loadTopic_inModel()
return {
topicList,
to,
}
},
components: {
EELayoutMainLayout,
},
})
</script>
A few digressions before solving the problem. Maybe you are a java developer. I personally think it is inappropriate to write the front end with Java ideas. The design of vue3's setup is more inclined to combined functional programming
To fully understand why you need some pre knowledge, Proxy and the get and set method of Object
They correspond to the two core apis in vue, reactive and ref,
The former can only be applied to objects( because proxy can only proxy objects),The latter can be applied to any type(primary for basic javascript types, get and set can apply for any type)
You can modify the code to meet your expectations
loadTopic_inModel() {
super.loadData('topics', { t_type_id: 1 }).then((re) => {
console.log(re.data)
this.topicList.value = re.data
})
}
You cannot modify a ref object directly, a test case to explain what is reactive
when ref function is called, a will be like be wrapped in a class has value properties, and has get and set method
the effect function will call the arrow function, and in this time, the get method of a will be called and it will track as a dependence of the effect function, when a changed, the set method of a will be called, and it will trigger the arrow function,
so when you direct modify the a, the setter method will never trigger, the view will not update
const a = ref(1)
let dummy
let calls = 0
effect(() => {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)

Use RTK Query with Graphql

So far I understand I need to build my own baseQuery. I could write graphql queries and mutations like in example here https://rtk-query-docs.netlify.app/examples/react-with-graphql, will I get full type safety for queries and mutations if I add types to query.builder like this builder.query<Device, void> or I must use something like this https://www.graphql-code-generator.com/docs/plugins/typescript-graphql-request#simple-request-middleware. In latter case how should my baseQuery look if I use generated hook for graphql-request library.
Here is example of hook from 2:
import { GraphQLClient } from 'graphql-request';
import { getSdk } from './sdk'; // THIS FILE IS THE GENERATED FILE
async function main() {
const client = new GraphQLClient('https://countries.trevorblades.com/');
const sdk = getSdk(client);
const { continents } = await sdk.continents(); // This is fully typed, based on the query
console.log(`GraphQL data:`, continents);
}
I am thinking something like:
import {getSdk} from './sdk'
const client = new GraphQLClient('https://countries.trevorblades.com/');
const graphqlBaseQuery = (someGeneratedQueryOrMutation, client) => {
const something = someGeneratedQueryOrMutation(client);
const { continents } = await something.continents();
return { data: continents };
};
Code does not really make sence but I hope you see where I am going with this. Thanks :)
Edit: By now there is a Grahql Codegen plugin available at https://www.graphql-code-generator.com/docs/plugins/typescript-rtk-query
Actually I started writing a plugin for the code generator a few days ago.
You can see the generated result here:
https://github.com/phryneas/graphql-code-generator/blob/5f9a2eefd81538782b791e0cc5df633935164a89/dev-test/githunt/types.rtk-query.ts#L406-L427
This would require you to create an api with a baseQuery using a graphql library of your choice like this.
A configuration would look like this
./dev-test/githunt/types.rtk-query.ts:
schema: ./dev-test/githunt/schema.json
documents: ./dev-test/githunt/**/*.graphql
plugins:
- typescript
- typescript-operations
- typescript-rtk-query
config:
importBaseApiFrom: '../../packages/plugins/typescript/rtk-query/tests/baseApi'
exportHooks: true
And I think for bundle-splitting purposes it would also work with the near-operation-file preset.
All that is not upstream yet - I will try to get that ready this weekend but don't know how much time it would take to actually get it in.
You could check the repo out, do a local build and install it with something like yalc though.
For a more basic approach without code generation you could look at this example or for an a bit more advanced setup (but also without full code generation, more integrated with existing tooling) you could look at this PR

Access JSON chunk exported from Gatsby Static Query

I have a React Component in a Gatsby app that is using the useStaticQuery hook to pull in data from the GraphQL layer. This component gets used in my application, but it also gets used as part of a JavaScript embed/widget that is created in a separate Webpack configuration.
I don't want the widget to depend on Gatsby, so I've shimmed the relevant bits of Gatsby, but I still need to pass in data to the shim I've created for useStaticQuery. I found that my Gatsby app is generating a file at public/static/d/2250905522.json that contains a perfect representation of the query data, and I'd like to use it like so:
// This file gets substituted when importing from `gatsby`
import queryResult from "../public/static/d/2250905522.json"
export const useStaticQuery = () => queryResult.data
export const graphql = () => {}
This works, but I haven't figured out where this is coming from or how to determine the file name in a way that is deterministic/stable. How is Gatsby determining this file name, and what internals might I use to do the same?
Edit: I found this routine in the Gatsby codebase that appears to be using staticQueryComponent.hash to determine the number. staticQueryComponent is being destructured from store.getState() where store is associated with Redux, but I'm still not sure where the hash is being determined yet.
Edit 2: Found another mention of this in the documentation here. It sounds like hash is a hash of the query itself, so this will change over time if the query changes (which is likely), so I'm still looking for the routine used to compute the hash.
Due to changes in the babel-plugin-remove-graphql-queries, coreyward's (awesome) answer should be updated to:
const { stripIgnoredCharacters } = require('graphql/utilities/stripIgnoredCharacters');
const murmurModule = require('babel-plugin-remove-graphql-queries/murmur');
const murmurhash = typeof murmurModule === 'function' ? murmurModule : murmurModule.murmurhash;
const GATSBY_HASH_SEED = 'abc';
function hashQuery(query) {
const result = murmurhash(stripIgnoredCharacters(query), GATSBY_HASH_SEED).toString();
return result;
}
module.exports = hashQuery;
The changes are:
fix the way murmurhash is imported. Credit to github user veloce, see: https://github.com/birkir/gatsby-source-graphql-universal/pull/16/files
Change to using stripIgnoredCharacters in order to match the updated way that gatsby internally hashes queries by first stripping whitespace and comment lines for efficiency.
Gatsby is using murmurhash with a seed of "abc" to calculate the hash of the full text of the query (including whitespace). This occurs in babel-plugin-remove-graphql-queries.
Since the reused components are isolated from Gatsby, the graphql tagged template literal can be shimmed in order to get the original query for hashing:
// webpack.config.js
module.exports = {
resolve: {
alias: {
gatsby: path.resolve(__dirname, "gatsby-shim.js"),
},
},
}
// gatsby-shim.js
import { murmurhash } from "babel-plugin-remove-graphql-queries/murmur"
import {
stripIgnoredCharacters,
} from "graphql/utilities/stripIgnoredCharacters"
const GATSBY_HASH_SEED = "abc"
const hashQuery = (query) =>
murmurhash(
stripIgnoredCharacters(query),
GATSBY_HASH_SEED
).toString()
export const graphql = query => hashQuery(query.raw[0])
This results in the query hash being passed into useStaticQuery, which can be shimmed similarly to retrieve the cached query from disk.
Also worth noting, newer versions of Gatsby store the StaticQuery result data in public/page-data/sq/d/[query hash].json.
If you're looking to do something similar, I've written up a much longer blog post about the details of this process and the solution I arrived at here.

How to add a simple SVG using D3 and ES2015

I have a ES2015 project using Webpack. I am trying to add D3v4 so I add the following...
// Typescript
import { select } from 'd3';
...
select(".application").append("svg"); // line 22
I have also tried...
import { select } from 'd3-selection';
But this throws an exception...
ERROR in ./src/main/angular/app/app.component.ts
(22,12): error TS1138: Parameter declaration expected.
What is the correct way to do this? Is there any D3 documentation for ES2015?

Problems with defining modules using AMD in Mocha

While writing tests I got bug TypeError: $.extend is not a function on toastr plugin that we are using. It seems that jQuery is not picked up properly, even tho is working normally in browser.
In our main mock file we imported jQuery and bind it to global windows object and it's accessible across whole application (but toastr plugin), even while testing in mocha:
import jsdom from 'jsdom';
import $ from 'jquery';
import chai from 'chai';
import chaiImmutable from 'chai-immutable';
import React from 'react';
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
const win = doc.defaultView;
global.document = doc;
global.window = win;
global.expect = chai.expect;
global.$ = $(win);
global.jquery = $(win);
global.jQuery = $(win);
Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key];
}
});
chai.use(chaiImmutable);
So while taking closer look at toastr I noticed this:
; (function (define) {
define(['jquery'], function ($) {
// this function is called on inizialization
function getOptions() {
return $.extend({}, getDefaults(), toastr.options);
}
It takes jquery from node_modules directly and then defines object $ in function scope, that means that it's ignoring window.$ (which is working normally even in here).
Therefore logging $ will return function, and logging $.anyMethodFromjQuery ($.extend) will return undefined.
In the end I tried logging $.prototype, in the browser it will return me jQuery object while in mocha it returns empty object {}
So in the end it define didn't created prototype in mocha environment and I cannot add one line of code $ = window.$; in plugin, since no one should edit a plugin + we are using npm.
Is there any solution for this?
You're running into trouble because you are loading code that should be loaded by JSDom outside of it. While it is possible in some cases to load code in Node and then pass it to the window that JSDom creates, that's a brittle approach, as you've discovered. Whenever I use JSDom for things other than write-and-toss cases, I load every script that would normally be loaded by a browser through JSDom. This avoids running into the issue you ran into. Here's a proof-of-concept based on the code you've shown in the question. You'll see that toastr.getContainer() runs fine, because Toastr has been loaded by JSDom. If you try to use the same code with an import toastr from "toastr" you'll get the same problem you ran into.
import jsdom from 'jsdom';
import $ from 'jquery';
import path from "path";
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>', {
features: {
FetchExternalResources: ["script"],
ProcessExternalResources: ["script"]
}
});
const win = doc.defaultView;
global.document = doc;
global.window = win;
global.$ = $(win);
global.jquery = $(win);
global.jQuery = $(win);
window.addEventListener("error", function () {
console.log("ERROR");
});
const script = document.createElement("script");
script.src = path.join(__dirname, "./node_modules/toastr/toastr.js");
document.head.appendChild(script);
// The load will necessarily be asynchronous, so we have to wait for it.
script.addEventListener("load", function () {
console.log("LOADED");
console.log(window.toastr);
// We do this here so that `toastr` is also picked up.
Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key];
}
});
toastr.getContainer();
});
Note that the code hung when I tried calling toastr.info(...). I took a look at the code of Toastr but it is not clear to me what causes the problem. There are features of browsers that JSDom is unable to emulate. It is possible that Toastr is dependent on such features. In the past, I've sometimes had to switch test suites away from JSDom due to its limitations.

Resources