Coder Social home page Coder Social logo

voltbras / ts-ocpp Goto Github PK

View Code? Open in Web Editor NEW
43.0 6.0 25.0 282 KB

:zap: OCPP (Open Charge Point Protocol) implemented in Typescript.

Home Page: https://voltbras.github.io/ts-ocpp

License: MIT License

TypeScript 98.78% JavaScript 1.22%
ocpp chargepoint typescript electric-vehicles functional-programming

ts-ocpp's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

ts-ocpp's Issues

Allow to subscribe to 'close' and 'error' transport events

The ChargePoint has a method connection(), the problem that I have is that if there is no connection problem and the connection, closes, I can't get this information and create proper logging trace, also the connect method binds console.log to error socket event, the CentralSistem has the same problem but has other implications:

One partial solution is this;

async connect(): Promise<WebSocket> {
    const url = `${this.csUrl}/${this.cpId}`;
    const socket = new WebSocket(url, SUPPORTED_PROTOCOLS);

    this.connection = new Connection(
      socket,
      this.requestHandler,
      centralSystemActions,
      chargePointActions,
    );
    socket.on('message', (data) => this.connection?.handleWebsocketData(data));

    return new Promise((resolve) => {
      socket?.on('open', (socket: WebSocket) => resolve(socket));
    });
  }

This doesn't break actual use, and provide the socket on open to allow binding to events an most important for me close event, that carry valuable information. I mean partially because I'm not happy exposing the socket and has a race condition in case that a error is raised after a chance to bind the events.

This also is valid, not expose socket, and doesn't break current API:

  async connect(
    errorHandler?: (err: any) => void,
    closeHandler?: (code: number, reason: string) => void,
  ): Promise<void> {
    const url = `${this.csUrl}/${this.cpId}`;
    const socket = new WebSocket(url, SUPPORTED_PROTOCOLS);

    if (errorHandler !== undefined) {
      socket.on('error', errorHandler);
    }
    if (closeHandler !== undefined) {
      socket.on('close', closeHandler);
    }

    this.connection = new Connection(
      socket,
      this.requestHandler,
      centralSystemActions,
      chargePointActions,
    );
    socket.on('message', (data) => this.connection?.handleWebsocketData(data));

    return new Promise((resolve) => {
      socket?.on('open', () => resolve());
    });
  }

I can make a PR with a solution adding a method on(), that can subscribe to this events or the solution that is more suited for project. I didn't make use of the CentralSistem code, but after reviewing it, I also find this problem to bind to this close/error events.

Auto-generated SOAP typings do not respect optional properties

For example the StartTransaction response, contains an idTagInfo field of IdTagInfo type.
As per the OCPP v1.5 SOAP specification, the fields parentIdTag and expiryDate of the IdTagInfo type are optional.
However they currently are mandatory via the types, but they should be optional. This occurs in various types that were generated per the WSDL files.

It would provide a more correct typing system, if either:

problem with BottNotificationResponse

Im trying to send BootNotificationResponse, as it is shown in examples:

defining the centralSystem:
const centralSystem = new OCPP.CentralSystem(3000, (req, metadata) => {

case 'BootNotification':
            return { 
                action: req.action, 
                ocppVersion,
                currentTime: new Date().toISOString(),
                payload: {
                    status: 'Accepted',
                    currentTime: new Date().toISOString(),
                    interval: 120
                }
            };

but when trying to run I get the following erro:

 error TS2345: Argument of type '(req: (AuthorizeRequest & { action: "Authorize"; ocppVersion: "v1.6-json"; }) | (BootNotificationRequest & { action: "BootNotification"; ocppVersion: "v1.6-json"; }) | ... 17 more ... | (IStopTransactionInput & { ...; }), metadata: RequestMetadata) => { ...; } | ... 1 more ... | { ...; }' is not assignable to parameter of type 'RequestHandler<"Authorize" | "BootNotification" | "DataTransfer" | "DiagnosticsStatusNotification" | "FirmwareStatusNotification" | "Heartbeat" | "MeterValues" | "StartTransaction" | "StatusNotification" | "StopTransaction", RequestMetadata, OCPPVersion>'.
  Type '{ action: "Heartbeat"; ocppVersion: "v1.6-json" | "v1.5-soap"; currentTime: string; payload?: undefined; } | { action: "StatusNotification"; ocppVersion: "v1.6-json" | "v1.5-soap"; currentTime?: undefined; payload?: undefined; } | { ...; }' is not assignable to type 'Result<(AuthorizeResponse & { action: "Authorize"; ocppVersion: "v1.6-json"; }) | (BootNotificationResponse & { action: "BootNotification"; ocppVersion: "v1.6-json"; }) | ... 17 more ... | (IStopTransactionOutput & { ...; })>'.
    Type '{ action: "BootNotification"; ocppVersion: "v1.6-json" | "v1.5-soap"; currentTime: string; payload: { status: string; currentTime: string; interval: number; }; }' is not assignable to type 'Result<(AuthorizeResponse & { action: "Authorize"; ocppVersion: "v1.6-json"; }) | (BootNotificationResponse & { action: "BootNotification"; ocppVersion: "v1.6-json"; }) | ... 17 more ... | (IStopTransactionOutput & { ...; })>'.
      Type '{ action: "BootNotification"; ocppVersion: "v1.6-json" | "v1.5-soap"; currentTime: string; payload: { status: string; currentTime: string; interval: number; }; }' is not assignable to type 'IHeartbeatOutput & { action: "Heartbeat"; ocppVersion: "v1.5-soap"; }'.
        Type '{ action: "BootNotification"; ocppVersion: "v1.6-json" | "v1.5-soap"; currentTime: string; payload: { status: string; currentTime: string; interval: number; }; }' is not assignable to type '{ action: "Heartbeat"; ocppVersion: "v1.5-soap"; }'.
          Types of property 'action' are incompatible.
            Type '"BootNotification"' is not assignable to type '"Heartbeat"'.

15 const centralSystem = new OCPP.CentralSystem(3000, (req, metadata) => {

What yould be the right way to define BootNotificationResponse?

Linter

About the public/private methods, how about adding a linter to this, so this restriction is enforced automatically?

Originally posted by @eduhenke in #7 (comment)

Can not StartTransaction

Hello,
I have some issues when try to StartTransaction, becuase StopTransaction Send me "DeAuthorized"
How can I resolve it?
This are my requests, responses and Code:

req = {
action: 'BootNotification',
ocppVersion: 'v1.6-json',
chargePointModel: 'CDT_TACW7::NET_WIFI',
chargePointVendor: 'ABB',
chargeBoxSerialNumber: 'TACW74',
firmwareVersion: 'TAC1Z9120',
meterType: 'V1'
}
chargePointId = TACW74
ret = {
action: 'BootNotification',
ocppVersion: 'v1.6-json',
status: 'Accepted',
interval: 1440,
currentTime: '2024-05-15T17:04:52.021Z'
}

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 0,
errorCode: 'NoError',
info: 'null',
status: 'Available',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'Preparing',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = { action: 'Authorize', ocppVersion: 'v1.6-json', idTag: '6CD98725' }
chargePointId = TACW74
ret = { idTagInfo: { status: 'Accepted' } }

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'SuspendedEV',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'Charging',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = {
action: 'StartTransaction',
ocppVersion: 'v1.6-json',
connectorId: 1,
idTag: '6CD98725',
meterStart: 0,
timestamp: '2024-05-15T17:05:12.000Z'
}
chargePointId = TACW74
ret = {
action: 'StartTransaction',
ocppVersion: 'v1.6-json',
idTagInfo: { satus: 'Accepted' },
transactionId: 739831
}

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'SuspendedEVSE',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'Finishing',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

req = {
action: 'StopTransaction',
ocppVersion: 'v1.6-json',
meterStop: 0,
idTag: '6CD98725',
timestamp: '2024-05-15T17:05:14.000Z',
transactionId: 739831,
reason: 'DeAuthorized'
}
chargePointId = TACW74
ret = {
action: 'StopTransaction',
ocppVersion: 'v1.6-json',
idTagInfo: { satus: 'Accepted' }
}

req = {
action: 'StatusNotification',
ocppVersion: 'v1.6-json',
connectorId: 1,
errorCode: 'NoError',
info: 'null',
status: 'Preparing',
vendorErrorCode: '0x0000'
}
chargePointId = TACW74
ret = { action: 'StatusNotification', ocppVersion: 'v1.6-json' }

This is my code:

    this.centralSystem = new CentralSystem(8100, (req, { chargePointId }) => {
      console.log('\n\n');
      console.log('req =', req);
      console.log('chargePointId =', chargePointId);
      let ret;

      switch (req.action) {
        case 'Heartbeat':
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
            currentTime: new Date().toISOString(),
          };
          break;
        case 'BootNotification':
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
            status: 'Accepted',
            interval: 1440,
            currentTime: new Date().toISOString(),
          };
          break;
        case 'StatusNotification':
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
          };
          break;
        case 'Authorize':
          ret = {
            idTagInfo: {
              status: 'Accepted',
            },
          };
          break;
        case 'StartTransaction':
          this.gTransactionId = this.getRandomNumber();
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
            idTagInfo: {
              satus: 'Accepted',
            },
            transactionId: this.gTransactionId,
          };
          break;
        case 'StopTransaction':
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
            idTagInfo: {
              satus: 'Accepted',
            },
          };
          break;
        case 'MeterValues':
          ret = {
            action: req.action,
            ocppVersion: req.ocppVersion,
          };
          break;
        default:
          throw new Error('message not supported');
      }

      console.log('ret =', ret);

      return ret;
    });

Should have way to get connecting Charge PointIds from Central System

I think it should have a way to know about connecting ChargePoints from Central System class.

When you run the Central System as container and connect it with other servers like this,

Charge Point <-> Central System <-> Your backend Server 

and you want send request to Charge Point, you don't know which Central System is connecting to this specific Charge Point.
Its better if Central System can let us know about connecting ChargePointIds.

Here is my idea,

const cs = new CentralSystem();
const chargePoints = cs.getChargePointIds();
const targetChargePointId = 111;
const connecting = !!chargePoints.filter(c => c === targetChargePointId).length;
if (connecting) {
  // has connection with target chargePoint.
  const response = await cs.sendRequest();
}
// no connection

TypeScript compile error

First of all, congratulations for this great library, I'm using it to emulate real CPs, I create a really simple project that creates a ChargePoint and connect to a real Central Server, when I ran it with ts-node it executes without problems, but when I try to compile with tsc i get this errors:

  • npm: 6.14.11
  • node: v12.21.0
  • tsc: 4.2.3
  • OS: Linux 5.4.0-67-generic #75~18.04.1-Ubuntu SMP Tue Feb 23 19:17:50 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ npm run build

> [email protected] build
> rimraf ./dist && tsc

node_modules/@voltbras/ts-ocpp/dist/messages/index.d.ts:6:93 - error TS2536: Type '"request"' cannot be used to index type 'ReqRes<T, V>'.

6 export declare type Request<T extends ActionName<V>, V extends OCPPVersion = OCPPVersion> = ReqRes<T, V>['request'];
                                                                                              ~~~~~~~~~~~~~~~~~~~~~~~

node_modules/@voltbras/ts-ocpp/dist/messages/index.d.ts:7:94 - error TS2536: Type '"response"' cannot be used to index type 'ReqRes<T, V>'.

7 export declare type Response<T extends ActionName<V>, V extends OCPPVersion = OCPPVersion> = ReqRes<T, V>['response'];
                                                                                               ~~~~~~~~~~~~~~~~~~~~~~~~

Found 2 errors.

I want to contribute to the library, I see that have some missing things, CI, lint, other ocpp-j and ocpp-s protocols, npm package, etc. I thing that could be great if this tasks will be created so people that want to help, have some clue on priority things.

TriggerMessage in sendRequest will throw Type error

Bug description

set TriggerMessage in sendRequest function of Central System will throw type error.

How to reproduce

  1. set TriggerMessage in action property of sendRequest function
import { CentralSystem } from '@voltbras/ts-ocpp';

const cs = new CentralSystem(8080, (req, metadata) => {
  const { ocppVersion } = req;
  switch (req.action) {
    case 'Heartbeat':
      return { action: req.action, ocppVersion, currentTime: new Date().toISOString() };
    case 'StatusNotification':
      return { action: req.action, ocppVersion };
  }
  throw new Error('not supported');
});

console.log('server started');

if (process.env.SEND_COMMAND) {
  setTimeout(async () => {
    console.log('sending request')
    const response = await cs.sendRequest({
      ocppVersion: 'v1.6-json',
//(property) action: "TriggerMessage"
// Type '"TriggerMessage"' is not assignable to type '"DataTransfer" | "CancelReservation" | "ChangeAvailability" | // "ChangeConfiguration" | "ClearCache" | "GetConfiguration" | "GetDiagnostics" | "GetLocalListVersion" | "RemoteStartTransaction" | ... 5 more ... | "UpdateFirmware"'.ts(2322)
      action: 'TriggerMessage', // type error 
      chargePointId: '123',
      payload: {
        connectorId: 2,
        idTag: '123',
      }
    });
    console.log({ response })
  }, 3000);
}

Expected behavior

No type error occur

Environment & setup

  • version: v2.7.3
  • OS: MacOS(M1 chip)
  • Node.js version: 16.9.1

Allow invalid(per the jsonschema) v1.6 requests to pass to the CS handler

Nowadays, when a request comes from the ChargePoint to the CentralSystem, the WS connection class, validates if the requests follows the jsonschema, if it doesn't it rejects the request and returns the error to the caller.

However, we interface with several different charge point models/vendors and some of them do not strictly follow the jsonschema(unfortunately).

It would be a nice addition if this library would allow these validation errors to be passed to the handler, and let the handler decide if that error is ok to ignore or not.

The project is not found on npm

Hello, I could not install the npm package because it is not found anymore on the npm. Can you help me regarding this problem please?
npm install @voltbras/ts-ocpp --save
npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/@voltbras%2fts-ocpp - Not found
npm ERR! 404
npm ERR! 404 '@voltbras/ts-ocpp@latest' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.

Thank you!

How to properly handle TriggerMessage flow?

I create a charge point like so:

export const cpHandlers: RequestHandler<
  CentralSystemAction<"v1.6-json">,
  ValidationError | undefined,
  "v1.6-json"
> = (req) => {
  switch (req.action) {
    case "TriggerMessage":
      return {
        status: "Accepted",
        action: "TriggerMessage",
        ocppVersion: req.ocppVersion,
      };
  }
  throw new Error("message not supported");
};


export const cp = new ChargePoint(
  chargerId,
  cpHandlers,
  `${wsUrl}/${chargerId}`

According to the specs, immediately after confirming TriggerMessage, I need to send the message being triggered.
What is the intended way to do this?

I can't seem to find a way to guarantee that it will be sent AFTER returning the confirmation for TriggerMessage.
I thought about adding a setTimeOut with the requested message before returning the confirmation, but it seems wrong.

No BootNotification in examples

As the title says - there's no BootNotification example for the server nor the client, and the BootNotification is most important message, as it starts whole communication between the server and the charger.

Unable to install the package via npm

npm is throwing error while installing the package:
npm ERR! prepareGitDep 1>
npm ERR! prepareGitDep > @voltbras/[email protected] prepare C:\Users\Saurabh\AppData\Roaming\npm-cache_cacache\tmp\git-clone-2a1ddc4a
npm ERR! prepareGitDep > npm run build
npm ERR! prepareGitDep
npm ERR! prepareGitDep
npm ERR! prepareGitDep > @voltbras/[email protected] build C:\Users\Saurabh\AppData\Roaming\npm-cache_cacache\tmp\git-clone-2a1ddc4a
npm ERR! prepareGitDep > tsc && npm run copy-assets
npm ERR! prepareGitDep
npm ERR! prepareGitDep
npm ERR! prepareGitDep > @voltbras/[email protected] copy-assets C:\Users\Saurabh\AppData\Roaming\npm-cache_cacache\tmp\git-clone-2a1ddc4a
npm ERR! prepareGitDep > npm run copy-assets-wsdl && npm run copy-assets-jsonschema
npm ERR! prepareGitDep
npm ERR! prepareGitDep
npm ERR! prepareGitDep > @voltbras/[email protected] copy-assets-wsdl C:\Users\Saurabh\AppData\Roaming\npm-cache_cacache\tmp\git-clone-2a1ddc4a
npm ERR! prepareGitDep > cp ./src/messages/soap/*.wsdl ./dist/messages/soap
npm ERR! prepareGitDep
npm ERR! prepareGitDep
npm ERR! prepareGitDep 2> npm WARN install Usage of the --dev option is deprecated. Use --also=dev instead.
npm ERR! prepareGitDep npm WARN deprecated [email protected]: request has been deprecated, see request/request#3142
npm ERR! prepareGitDep npm WARN deprecated [email protected]: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added
npm ERR! prepareGitDep npm WARN deprecated [email protected]: this library is no longer supported
npm ERR! prepareGitDep npm WARN deprecated [email protected]: CoffeeScript on NPM has moved to "coffeescript" (no hyphen)
npm ERR! prepareGitDep npm WARN deprecated [email protected]: Removed event-stream from gulp-header
npm ERR! prepareGitDep npm WARN deprecated [email protected]: https://github.com/lydell/resolve-url#deprecated
npm ERR! prepareGitDep npm WARN deprecated [email protected]: Please see https://github.com/lydell/urix#deprecated
npm ERR! prepareGitDep 'cp' is not recognized as an internal or external command,
npm ERR! prepareGitDep operable program or batch file.
npm ERR! prepareGitDep npm ERR! code ELIFECYCLE
npm ERR! prepareGitDep npm ERR! errno 1
npm ERR! prepareGitDep npm ERR! @voltbras/[email protected] copy-assets-wsdl: cp ./src/messages/soap/*.wsdl ./dist/messages/soap
npm ERR! prepareGitDep npm ERR! Exit status 1
npm ERR! prepareGitDep npm ERR!
npm ERR! prepareGitDep npm ERR! Failed at the @voltbras/[email protected] copy-assets-wsdl script.
npm ERR! prepareGitDep npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! prepareGitDep
npm ERR! prepareGitDep npm ERR! A complete log of this run can be found in:
npm ERR! prepareGitDep npm ERR! C:\Users\Saurabh\AppData\Roaming\npm-cache_logs\2021-08-26T07_29_14_464Z-debug.log
npm ERR! prepareGitDep npm ERR! code ELIFECYCLE
npm ERR! prepareGitDep npm ERR! errno 1
npm ERR! prepareGitDep npm ERR! @voltbras/[email protected] copy-assets: npm run copy-assets-wsdl && npm run copy-assets-jsonschema
npm ERR! prepareGitDep npm ERR! Exit status 1
npm ERR! prepareGitDep npm ERR!
npm ERR! prepareGitDep npm ERR! Failed at the @voltbras/[email protected] copy-assets script.
npm ERR! prepareGitDep npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! prepareGitDep
npm ERR! prepareGitDep npm ERR! A complete log of this run can be found in:
npm ERR! prepareGitDep npm ERR! C:\Users\Saurabh\AppData\Roaming\npm-cache_logs\2021-08-26T07_29_14_500Z-debug.log
npm ERR! prepareGitDep npm ERR! code ELIFECYCLE
npm ERR! prepareGitDep npm ERR! errno 1
npm ERR! prepareGitDep npm ERR! @voltbras/[email protected] build: tsc && npm run copy-assets
npm ERR! prepareGitDep npm ERR! Exit status 1
npm ERR! prepareGitDep npm ERR!
npm ERR! prepareGitDep npm ERR! Failed at the @voltbras/[email protected] build script.
npm ERR! prepareGitDep npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! prepareGitDep
npm ERR! prepareGitDep npm ERR! A complete log of this run can be found in:
npm ERR! prepareGitDep npm ERR! C:\Users\Saurabh\AppData\Roaming\npm-cache_logs\2021-08-26T07_29_14_540Z-debug.log
npm ERR! prepareGitDep npm ERR! code ELIFECYCLE
npm ERR! prepareGitDep npm ERR! errno 1
npm ERR! prepareGitDep npm ERR! @voltbras/[email protected] prepare: npm run build
npm ERR! prepareGitDep npm ERR! Exit status 1
npm ERR! prepareGitDep npm ERR!
npm ERR! prepareGitDep npm ERR! Failed at the @voltbras/[email protected] prepare script.
npm ERR! prepareGitDep npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! prepareGitDep
npm ERR! prepareGitDep npm ERR! A complete log of this run can be found in:
npm ERR! prepareGitDep npm ERR! C:\Users\Saurabh\AppData\Roaming\npm-cache_logs\2021-08-26T07_29_14_631Z-debug.log
npm ERR! prepareGitDep
npm ERR! premature close

Improve docs

Improve project documentation including:

  • examples
  • CONTRIBUTING.md
  • Better README.md

SOAP and JSON Schemas are outdated

In most cases, it doesn't change anything, though the problem I saw was with the MeterValues - this module uses outdated schemas, without Celsius as MeterValues unit, whereas newer MeterValues JSON schema includes it

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.