mghoneimy / php-graphql-client Goto Github PK
View Code? Open in Web Editor NEWA PHP library that simplifies the process of interacting with GraphQL API's by providing simple client and query generator classes.
License: MIT License
A PHP library that simplifies the process of interacting with GraphQL API's by providing simple client and query generator classes.
License: MIT License
I am using your plugin to query graphql api and i can't find a way to pass array of strings as argument using QueryBuilder and RawObject.
$qb->setVariable('providerSlugs', '[String]', true);
$variables['providerSlugs'] = ['provider1', 'provider2']; //This is array e.g. array('provider1', 'provider2')
$qb->setArgument('where', new RawObject( '{providers :{slug : $providerSlugs}}'));
print_r($queryParams->getVariables()); //this prints out : Array ( [limit] => 20 [sort] => rating:desc [providerSlugs] => Array ( [0] => provider1 ) )
$response = $client->runQuery($queryParams->getQuery(), true, $queryParams->getVariables());
The result is returned without the filtering, it's just that this particular "where" filter is not taken in mind. The graphql query get constructed correctly, however the where filtering doesn't work when it's executed from the $client. If i execute it from Playground client in the browser, it works fine.
webonyx/graphql-php
is a popular PHP GraphQL server library. I was going to use this module until I discovered that it uses the namespace GraphQl and directly conflicts with webonyx/graphql-php
. Any plans to put this under a namespace like MGhoneimy\GraphQl\
? This is the only way I could use this module.
Hello,
I've found a little issue in your code : the methods GraphQL\Client::runQuery
and GraphQL\Client::runRawQuery
are type-hinted ?Results
, so they are supposed to return a GraphQL\Results
or null
, but in fact these methods can't return null
(or I'm missing a point).
Is it possible to change the return type-hint to simply Results
?
Thanks and congrats for your awesome job!
At the moment the request type is hardcoded to POST (see here). Many applications don't cache POST requests, but only GET requests. For non sensitive data it's totally fine to use GET requests.
I'd propose to not hardcode the request method but instead add an option to set the method when executing a query. Adding a optional parameter to the runQuery method would solve this and also not require a new major release as it would be backwards compatible.
Any thoughts?
If you want me to I can solve the problem and make a PR for this feature.
Hi,
I'm trying to convert this raw query to retrieve the list of enumValues in schema, but couldnt figure out how:
query MyQuery {
__type(name: "Type") {
enumValues {
name
}
}
}
The enum in schema is:
enum Type {
Type1
Type2
Type3
}
I tried a few combination but cant get it to work, here is one example:
$query = (new \GraphQL\Query('__type'))
->setSelectionSet([
(new \GraphQL\Query('enumValues'))
->setSelectionSet(['name'])
])
->setArguments([
'name' => new RawObject('Type'),
]);
Does anyone know how to do this in Query object instead of raw query?
Thanks!
I was wondering whether you'd be willing to drop the hard dependency in Guzzle and make the HTTP client interchangeable. I know Guzzle is one of the (if not THE) most popular HTTP clients out there, but I'm not a big fan of the fact that most libraries will have a dependency on some specific version of Guzzle and it can be a pain in the neck to align the versions so everybody can be happy.
Looking at the code, I didn't see anything that wouldn't be possible with the PSR-18 interface and I believe the switch could be implemented so it requires only a minimal "migration" on the end-user side - users would have to install guzzle (and a guzzle -> PSR-18 adapter) explicitly.
I'd like to use this library for its Query/Mutation classes (I don't really care for the Client class), but I'd prefer to not bring Guzzle in as a dependency.
I'd be happy to do the work and submit a PR, but before I start, I wanted to see if there is any chance of it being accepted.
I need to use a verification token, where that value goes?
$client = new Client(
'http://192.168.57.104:3000/graphql',
[] // Replace with array of extra headers to be sent with request for auth or other purposes
);
Im trying to install this package in a php 8 project but its not supported, do you know the status of compatibility with php 8?
Hey,
We've been experiencing the following issue: array_key_exists(): Argument #2 ($array) must be of type array, null given
It seems that for whatever reason, the server on the other end (well outside of our control) is flaky and every now and then would fail. Then, trying to json_decode fails and null is returned in the constructor of Results
, so array_key_exists then fails.
I'd suggest using strict json_decode throwing an exception, but in that case, my concern would be actually also exposing the response contents as a string, so we could at least ping our integrations with a "hey, this and that is going on with the API". Otherwise, they usually tell us "it's running fine".
Would you consider extending QueryError
to also include the response as string (possibly nullable?), and in case the json_decode
failed, instantiating it with an internal "fake" error array such as $errorDetails['errors'][0]['message'] = $jsonDecodeException->getMessage()
and the response?
That way we'll know what happened on the other side. As it currently is, we get 500s and while we can figure out it's the other end that's faulty, we can't really help but let them know, and we do not have any sane way of handling the error gracefully besides catching all TypeError (which could potentially hide other genuine bugs).
I'm willing to do the job, as long as the proposal is OK with you.
Results constructor down here for quick reference:
public function __construct(ResponseInterface $response, $asArray = false)
{
$this->responseObject = $response;
$this->responseBody = $this->responseObject->getBody()->getContents();
$this->results = json_decode($this->responseBody, $asArray);
// Check if any errors exist, and throw exception if they do
if ($asArray) $containsErrors = array_key_exists('errors', $this->results);
else $containsErrors = isset($this->results->errors);
if ($containsErrors) {
// Reformat results to an array and use it to initialize exception object
$this->reformatResults(true);
throw new QueryError($this->results);
}
}
Hi!
How could I use the classes to set up a request like the one below?
mutation createResponse{
createResponse(
industry_code:"FAB"
order_code:79995
wholesaler_code:"000000000000"
processed_at:"2021-03-11 15:32:00"
reason: ORDER_SUCCESSFULLY_ACCEPTED
consideration: "101"
products:[{
ean:"7897337706162"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
]
){
id
content
imported_at
outcome
}
}
Hello,
I am currently using this package and currently trying to connect to a graphQL client.
I have this setArguments which the types is in ENUM. below:
->setArguments([ 'types' => 'GENERAL', 'languages' => ["en"]])
I tried to sent the request to the graphQl client but I always get this message.
"message": "Expected type DescriptionType!, found \"GENERAL\". Did you mean the enum value GENERAL?
I just want to ask how do you send Enum Values in a grapql client using setArguments or do i need to use the setVariables?
Can you please show an example?
Thank you!
Hey,
I'm asking myself if it would make sense to add the possibility to utilize AWS IAM signed requests in this package (optional).
I have a project currently where the authorization needs to happen with AWS SignatureV4 signing
The idea is to add AWS IAM authorization support, so the client could be used with AWS appsync using this auth method.
Select auth type method (with header, request signing, etc.) could be used.
AWS php sdk would be useful for signing request. Signing code example:
// set up key, secret, region
$credentials = new \Aws\Credentials\Credentials($key, $secret);
$signature = new \Aws\Signature\SignatureV4('appsync', $region);
$request = $signature->signRequest($request, $credentials, 'appsync');
@mghoneimy what do you think about it? I could prepare PR shortly if you are fine with this feature.
Hello!
I suggest to remove an empty selection set from a final query.
For example:
(new Query('test'))->setArguments(['foo' => "bar"]);
generates the query:
query {
test(foo: "bar") {
}
}
which in my case causes an error:
Field "test" must not have a selection since type "JSON!" has no subfields.
Meanwhile, the next query works fine:
query {
test(foo: "bar")
}
WDYT?
When passing strings as attributes to a Query the resulting strings in the gql are not escaped.
(new Query('users'))->setArguments(['name' => 'Lester "Lessie" McMillian'])
Currently:
query {users(name: "Lester "Lessie" McMillian") {}}
Expected:
query {users(name: "Lester \"Lessie\" McMillian") {}}
Test:
public function testItEscapesStringArguments()
{
$result = (new Query('users'))
->setArguments(['name' => 'Lester "Lessie" McMillian']);
$this->assertEquals("query {\nusers(name: \"Lester \\\"Lessie\\\" McMillian\") {\n\n}\n}", (string)$result);
}
Hi there, could someone please tell me how to use this library to make the follow query?
{
nodes(ids: ["gid://shopify/Product/123", "gid://shopify/Product/456"]) {
...on Product {
title
}
}
}
Hi! Given a populated Query or QueryBuilder object, is it possible to print the raw query that generated the request?
E.G.:
$builder = (new QueryBuilder('pokemon'))
->setArgument('name', 'Pikachu')
->selectField('id')
->selectField('name')
->selectField(
(new QueryBuilder('attacks'))
->selectField(
(new QueryBuilder('special'))
->selectField('name')
->selectField('type')
->selectField('damage')
)
)
->selectField(
(new QueryBuilder('evolutions'))
->selectField('id')
->selectField('name')
->selectField('number')
->selectField(
(new QueryBuilder('attacks'))
->selectField(
(new QueryBuilder('fast'))
->selectField('name')
->selectField('type')
->selectField('damage')
)
)
);
try {
$results = $client->runQuery($builder);
}
catch (QueryError $exception) {
var_dump($builder->getRawQuery()); // <<<< THIS
print_r($exception->getErrorDetails());
exit;
}
This would be very useful when something goes wrong and you need the raw query to test it on a GraphQL desktop client (Altair in my case).
Thanks!
Hello,
I didnt see any example or test for a update mutation, how can be implemented this mutation:
mutation MyMutation { update_users(where: {id: {_eq: 1}}, _set: {name: "My name"}) { affected_rows } }
Thank you
According to spec (http://spec.graphql.org/October2021/#sec-Errors.Error-result-format), GraphQL response can contain both errors and data at the same time. With current implementation this package throws the QueryError
exception which only contains the first error details. So, right now there is no way to access the data that came with the error response.
The constructor of QueryError
exception requires the full response. So may be we can have a property in this exception that will contain data
?
My app's phpstan picked up this tiny error:
https://github.com/mghoneimy/php-graphql-client/blob/master/src/Util/StringLiteralFormatter.php
/**
* Converts the value provided to the equivalent RHS value to be put in a file declaration
*
* @param string|int|float|bool $value
*
* @return string
*/
public static function formatValueForRHS($value): string
should be:
/**
* Converts the value provided to the equivalent RHS value to be put in a file declaration
*
* @param string|int|float|bool|null $value
*
* @return string
*/
public static function formatValueForRHS($value): string
Null isn't being allowed to be sent to that function.
Hello Team,
I'm stucking with setSelectionSet for nested set, please check sample Query below:
`query {
account {
request_id
complexity
data {
warehouses {
id
company_name
address {
name
state
}
}
}
}
}`
Hi, I'm working on calling a GraphQL API and I found this package was very useful, thanks for making it !
However, I encountered a little problem :
It seems like the query is automatically completed with a query
keyword at the beginning of my query string as in this snippet :
query { content { product(id: xx) { id } } }
Is it because of some GraphQL rule ?
In fact it blocks me from calling the API since my query should begin with the content
keyword.
Would you be able to give choice about that automatic keyword insertion ?
We have the option to use the QueryBuilder
for queries, why not for a mutation? :)
I added the possibilty to make a GET request (#39). But now I saw that i completly fucked it up. I'm so sorry for that.
GET requests cannot work as the query itself is never attached to the uri which has to happen of course if you want to make a GET request. If you try to execute a GET request at the moment you'll reveice the error GraphQL Request must include at least one of those two parameters: "query" or "queryId"
.
The solution would be to add the query and it's variables to the request url, here. I already tried it locally but I'm to incompetent to make it work.
Again, sorry for fucking this thing up so hard.
Line 122:
$request = $request->withBody(Psr7\stream_for(json_encode($bodyArray)));
Should be changed to:
$request = $request->withBody(Psr7\Utils::streamFor(json_encode($bodyArray)));
QueryBuilder does not provide the ability to select fields with alias names nor to provide argument lists for selected fields.
We need to add support for both, which will make the package more powerful and empower future features in https://github.com/mghoneimy/php-graphql-oqm package.
Hi all,
in the REAME.md in the chapter Constructing The Client
is written the following:
The Client constructor also receives an optional "httpOptions" array, which overrides the "authorizationHeaders" and can be used to add custom Guzzle HTTP Client request options.
With the following sample code:
$client = new Client(
'http://api.graphql.com',
[],
[
'connect_timeout' => 5,
'timeout' => 5,
'headers' => [
'Authorization' => 'Basic xyz'
'User-Agent' => 'testing/1.0',
],
'proxy' => [
'http' => 'tcp://localhost:8125', // Use this proxy with "http"
'https' => 'tcp://localhost:9124', // Use this proxy with "https",
'no' => ['.mit.edu', 'foo.com'] // Don't use a proxy with these
],
'cert' => ['/path/server.pem', 'password']
...
]
);
But that does not match the actual constructor of the class which look like this:
/**
* Client constructor.
*
* @param string $endpointUrl
* @param array $authorizationHeaders
*/
public function __construct(string $endpointUrl, array $authorizationHeaders = [])
{
$this->endpointUrl = $endpointUrl;
$this->authorizationHeaders = $authorizationHeaders;
$this->httpClient = new \GuzzleHttp\Client();
}
There is no third constructor parameter. There is nothing passed to guzzle. Is the class constructor wrong or is the documentation outdated/incorrect?
Thanks right now.
There some issue when i try to query data with nested level of filters for example
farmsByFarmerId: {aggregates: {distinctCount: {id: {equalTo: "3"}}}}
it through array to string conversion error
could you please check it .
Why is ext-readline required by the package? The only occurence is in README.md in examples. But it is nowhere in the code of the library (or at least i could not find it).
$ find . -type f -exec grep -nH readline "{}" \;
./README.md:437: $name = readline('Enter pokemon name: ');
./README.md:476: $name = readline('Enter pokemon name: ');
./composer.json:34: "ext-readline": "*",
Great library!
However, I can not seem to find a way to add to the base URL
I have set?
This is my client:
return new Client(
'https://' . config('shopify.store_name') . '.myshopify.com',
['X-Shopify-Access-Token' => config('shopify.password')]
);
Is there a method to extend the URL without having to change it in the client - it limits its reusability.
Thanks in advance.
Please, add subscription to your graphql library
Hi.
Why is it necessary to provide the type of variable in client via "new Variable()"?
The GraphQL server already defines which type is needed and throws an error if the type is not correct.
Hello, I think there is a problem on how empty strings are handled, they are sent as null to the server.
This is the inout I'm sending:
$mutation->setVariables([
new Variable('input', $input_type, true)
])
->setArguments(['input' => '$input']);
Which generates the following request:
'query' => 'mutation($input: UpdateHotelInput!) {
updateHotel(input: $input) {
id
}
}',
'variables' =>
array (
'input' =>
array (
'id' => '5',
'settings' =>
array (
'syncWithoutDetaching' =>
array (
0 =>
array (
'id' => 35,
'value' => '',
),
),
),
),
)
But I'm getting this error from the server:
Expected non-nullable type String! not to be null at value.settings.syncWithoutDetaching[0].value.
I hope you can help me
Thanks
Carlos
Type error: Argument 1 passed to GuzzleHttp\Client::send() must be an instance of GuzzleHttp\Message\RequestInterface, instance of GuzzleHttp\Psr7\Request given, called in /var/www/html/modules/solarreseller/vendor/gmostafa/php-graphql-client/src/Util/GuzzleAdapter.php on line 41
Hi,
would it be possible to add the possibility to have multiple queries within one QueryObject?
Our API enables us to fetch multiple queries at once like this:
query ($where:PeopleWhereInput, $orderBy:PeopleOrderByInput, $start:Int, $length:Int, $locales:[Locale!]!){
peoples (
where:$where,
orderBy:$orderBy
first:$length
skip:$start
locales:$locales
) {
id
displayName
createdAt
imageTemp
creator
stage
emailAddress
phoneNumber
company
peopleType
locale
slug
}
peoplesConnection{aggregate{count}}
results: peoplesConnection(
where:$where,
orderBy:$orderBy
){aggregate{count}}
}
Maybe it is possible already and I just didn't get it.
I'm using the example for my code but it shows me this error. What is the problem?
$mutation = (new Mutation('createOBT_MOTIVO_VIAJE'))
->setArguments(['OBT_MOTIVOS_VIAJEEntry!' => new RawObject('{NOMBRE_TIPO_MOTIVO: "prueba1", DESCRIPCION_TIPO_MOTIVO: "prueba1"}')])
->setSelectionSet(
[
'NOMBRE_TIPO_MOTIVO',
'DESCRIPCION_TIPO_MOTIVO',
]
);
//var_dump($mutation);exit;
$results = $client->runQuery($mutation);
This is the error:
Syntax Error: Expected :, found !
Hello!
When I am trying create Mutation with argument value null
, Query::constructArguments() makes empty value string in query.
For example arguments: ['foo' => 1, 'bar' => null, 'some' => '1']
Result: (foo: 1 bar: some: "1")
instead of (foo: 1 bar: null some: "1")
It is becouse null
is not sclar, and constructArguments()
makes simple type conversion null
to string
.
Thank Guys! Very helpful library!
Hi there. I'm trying to produce the following mutation with this library:
mutation { purchaseOrders { accept ( poNumber: "TEST_94736163", shipSpeed: GROUND, lineItems: [ { partNumber: "JE10394E", quantity: 1, unitPrice: 9.99, estimatedShipDate: "2022-07-13 11:07:03.000000 -04:00" } ] ) { handle, submittedAt, errors { key, message } } } }
The additional layer accept
is making it very difficult. At the moment I'm having to use the runRawQuery
method in order to produce the above.
The closest I've come by using mutation with variables
is this:
mutation($poNumber: String! $shipSpeed: ShipSpeed! $lineItems: AcceptedLineItemInput!) { purchaseOrders(poNumber: "TEST_94736163" shipSpeed: "GROUND" lineItems: ["test"]) }
However, the brackets are not in the right places.
I've seen the issue raised here but this does not help unfortunately.
If anyone has any ideas, please let me know.
Kind regards
Jason
How do I upload a file?
mutation CreateDocumentMutation( $document: DocumentInput!, $signers: [SignerInput!]!, $file: Upload! ) { createDocument( sandbox: true, document: $document, signers: $signers, file: $file ) { id, processed_at } }
We have a case where attempting to run a mutation using string values that may begin with a dollar sign ($). I believe the library always treats strings like this as variables, so they're not encapsulated in quotes in the raw GraphQL query.
Running a mutation with arguments like this: ['item_name' => '$500 test value']
The raw graphql ends up looking like:
mutation {
create_item(item_name: $500 test value) { id }
}
Which results in this error:
GraphQL\Exception\QueryError: Parse error on "500" (INT) at [2, 45] in /var/task/vendor/gmostafa/php-graphql-client/src/Results.php:55
I believe this is due to the check here: https://github.com/mghoneimy/php-graphql-client/blob/master/src/Util/StringLiteralFormatter.php#L22-L23
Any suggestions for a workaround, or a way to support this?
Hello @mghoneimy ,
I just want to know why there are no Mutation Class and Query Builder in the lower version
of this package?
Currently, when we require this package via composer the below version is the one that we
are using since we are using php version 7.0..
"gmostafa/php-graphql-client": "^0.3.5"
When we tried using query builder and mutation it always say Mutation and Query Builder class not found.
Checking the package the class for mutation and query builder is missing.
Hi, there's a way or example of how i can consume a api graphql where there is pagination?
I'm trying consuming this news api:
{
news {
data {
id
subject
body
imageUrl
createdAt
category {
id
description
icon
color
}
}
total
per_page
}
}
I tried this examples, but doesn't works. Can you me give a help?
$query = (new Query('news'))
->setSelectionSet([
'id',
'subject',
'body',
'createdAt',
'imageUrl',
]);
$query = (new Query('news'))
->setSelectionSet([
'data' => [
'id',
'subject',
'body',
'createdAt',
'imageUrl',
]
]);
Tks!
Hi, thanks for the library. Great so far. I was just wondering if you have plans to add support for variables in the future?
Hi!
How could I use the classes to set up a request like the one below?
mutation createResponse{
createResponse(
industry_code:"FAB"
order_code:79995
wholesaler_code:"000000000000"
processed_at:"2021-03-11 15:32:00"
reason: ORDER_SUCCESSFULLY_ACCEPTED
consideration: "101"
products:[{
ean:"7897337706162"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
{
ean:"7897337706506"
response_amount: 5
unit_discount_percentage:10.00
unit_discount_value:3.29
unit_net_value:29.69
monitored:false
industry_consideration: "000"
percentage_discount_return:10.00
icms_percentage_transfer:1.50
source_product:5
}
]
){
id
content
imported_at
outcome
}
}
Is it possible to use interfaces with this library? For example the ... on Tree
syntax in the following snippet from the GitHub API:
query {
repository(owner: "vendor", name: "package") {
object(expression: "master:src/") {
... on Tree {
entries {
name
}
}
}
}
}
Thanks for a really useful package!
We noticed an "Array to string conversion" bug with some of our queries. Not sure if we made the right fix, but we ended up overriding a few classes in order to fix.
GraphQl\Query::constructArguments
:
GraphQL\Util\StringLiteralFormatter
:
Added these methods:
/**
* @param array $array
*
* @return string
*/
public static function formatArrayForGQLQuery(array $array): string
{
$arrString = '{';
$first = true;
foreach ($array as $name => $element) {
if ($first) {
$first = false;
} else {
$arrString .= ', ';
}
if (is_array($element)) {
$arrString .= $name . ':';
if (array_keys($element) !== range(0, count($element) - 1)) {
$arrString .= static::formatAssociativeArray($element);
} else {
$arrString .= static::formatSequentialArray($element);
}
} else {
$arrString .= $name . ':' . \GraphQL\Util\StringLiteralFormatter::formatValueForRHS($element);
}
}
$arrString .= '}';
return $arrString;
}
/**
* @param $array
* @return string
*/
public static function formatSequentialArray($array): string
{
$arrString = '[';
foreach ($array as $value) {
$arrString .= static::formatAssociativeArray($value);
}
$arrString .= ']';
return $arrString;
}
/**
* @param $array
* @return string
*/
public static function formatAssociativeArray($array): string
{
$arrString = '{';
$first = true;
foreach ($array as $key => $val) {
if ($first) {
$first = false;
} else {
$arrString .= ', ';
}
if (is_array($val)) {
$arrString .= $key . ':';
if (array_keys($val) !== range(0, count($val) - 1)) {
$arrString .= static::formatAssociativeArray($val);
} else {
$arrString .= static::formatSequentialArray($val);
}
} else {
$arrString .= $key . ':' . \GraphQL\Util\StringLiteralFormatter::formatValueForRHS($val);
}
}
$arrString .= '}';
return $arrString;
}
Not sure if this is the right approach or if we are using this incorrectly, but this did get us past the issues we were running into.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.